Completed
Pull Request — develop (#1708)
by Aristeides
15:56 queued 13:45
created

wp.customize.Control.extend.kirkiValidateCSSValue   D

Complexity

Conditions 9
Paths 4

Size

Total Lines 27

Duplication

Lines 27
Ratio 100 %

Importance

Changes 0
Metric Value
cc 9
nc 4
nop 1
dl 27
loc 27
rs 4.909
c 0
b 0
f 0
1
/* jshint -W079 */
2
/* jshint unused:false */
3
if ( _.isUndefined( window.kirkiSetSettingValue ) ) {
4
	var kirkiSetSettingValue = { // jscs:ignore requireVarDeclFirst
5
6
		/**
7
		 * Set the value of the control.
8
		 *
9
		 * @since 3.0.0
10
		 * @param string setting The setting-ID.
11
		 * @param mixed  value   The value.
12
		 */
13
		set: function( setting, value ) {
14
15
			/**
16
			 * Get the control of the sub-setting.
17
			 * This will be used to get properties we need from that control,
18
			 * and determine if we need to do any further work based on those.
19
			 */
20
			var $this = this,
21
			    subControl = wp.customize.settings.controls[ setting ],
22
			    valueJSON;
23
24
			// If the control doesn't exist then return.
25
			if ( _.isUndefined( subControl ) ) {
26
				return true;
27
			}
28
29
			// First set the value in the wp object. The control type doesn't matter here.
30
			$this.setValue( setting, value );
31
32
			// Process visually changing the value based on the control type.
33
			switch ( subControl.type ) {
34
35
				case 'kirki-background':
36
					if ( ! _.isUndefined( value['background-color'] ) ) {
37
						$this.setColorPicker( $this.findElement( setting, '.kirki-color-control' ), value['background-color'] );
38
					}
39
					$this.findElement( setting, '.placeholder, .thumbnail' ).removeClass().addClass( 'placeholder' ).html( 'No file selected' );
40
					_.each( ['background-repeat', 'background-position'], function( subVal ) {
41
						if ( ! _.isUndefined( value[ subVal ] ) ) {
42
							$this.setSelectWoo( $this.findElement( setting, '.' + subVal + ' select' ), value[ subVal ] );
43
						}
44
					});
45
					_.each( ['background-size', 'background-attachment'], function( subVal ) {
46
						jQuery( $this.findElement( setting, '.' + subVal + ' input[value="' + value + '"]' ) ).prop( 'checked', true );
47
					});
48
					valueJSON = JSON.stringify( value ).replace( /'/g, '&#39' );
49
					jQuery( $this.findElement( setting, '.background-hidden-value' ).attr( 'value', valueJSON ) ).trigger( 'change' );
50
					break;
51
52
				case 'kirki-code':
53
					jQuery( $this.findElement( setting, '.CodeMirror' ) )[0].CodeMirror.setValue( value );
54
					break;
55
56
				case 'checkbox':
57
				case 'kirki-switch':
58
				case 'kirki-toggle':
59
					value = ( 1 === value || '1' === value || true === value ) ? true : false;
60
					jQuery( $this.findElement( setting, 'input' ) ).prop( 'checked', value );
61
					wp.customize.instance( setting ).set( value );
62
					break;
63
64
				case 'kirki-select':
65
				case 'kirki-preset':
66
				case 'kirki-fontawesome':
67
					$this.setSelectWoo( $this.findElement( setting, 'select' ), value );
68
					break;
69
70
				case 'kirki-slider':
71
					jQuery( $this.findElement( setting, 'input' ) ).prop( 'value', value );
72
					jQuery( $this.findElement( setting, '.kirki_range_value .value' ) ).html( value );
73
					break;
74
75
				case 'kirki-generic':
76
					if ( _.isUndefined( subControl.choices ) || _.isUndefined( subControl.choices.element ) ) {
77
						subControl.choices.element = 'input';
78
					}
79
					jQuery( $this.findElement( setting, subControl.choices.element ) ).prop( 'value', value );
80
					break;
81
82
				case 'kirki-color':
83
					$this.setColorPicker( $this.findElement( setting, '.kirki-color-control' ), value );
84
					break;
85
86
				case 'kirki-multicheck':
87
					$this.findElement( setting, 'input' ).each( function() {
88
						jQuery( this ).prop( 'checked', false );
89
					});
90
					_.each( value, function( subValue, i ) {
91
						jQuery( $this.findElement( setting, 'input[value="' + value[ i ] + '"]' ) ).prop( 'checked', true );
92
					});
93
					break;
94
95
				case 'kirki-multicolor':
96
					_.each( value, function( subVal, index ) {
97
						$this.setColorPicker( $this.findElement( setting, '.multicolor-index-' + index ), subVal );
98
					});
99
					break;
100
101
				case 'kirki-radio-buttonset':
102
				case 'kirki-radio-image':
103
				case 'kirki-radio':
104
				case 'kirki-dashicons':
105
				case 'kirki-color-palette':
106
				case 'kirki-palette':
107
					jQuery( $this.findElement( setting, 'input[value="' + value + '"]' ) ).prop( 'checked', true );
108
					break;
109
110
				case 'kirki-typography':
111
					_.each( ['font-family', 'variant', 'subsets'], function( subVal ) {
112
						if ( ! _.isUndefined( value[ subVal ] ) ) {
113
							$this.setSelectWoo( $this.findElement( setting, '.' + subVal + ' select' ), value[ subVal ] );
114
						}
115
					});
116
					_.each( ['font-size', 'line-height', 'letter-spacing', 'word-spacing'], function( subVal ) {
117
						if ( ! _.isUndefined( value[ subVal ] ) ) {
118
							jQuery( $this.findElement( setting, '.' + subVal + ' input' ) ).prop( 'value', value[ subVal ] );
119
						}
120
					});
121
122
					if ( ! _.isUndefined( value.color ) ) {
123
						$this.setColorPicker( $this.findElement( setting, '.kirki-color-control' ), value.color );
124
					}
125
					valueJSON = JSON.stringify( value ).replace( /'/g, '&#39' );
126
					jQuery( $this.findElement( setting, '.typography-hidden-value' ).attr( 'value', valueJSON ) ).trigger( 'change' );
127
					break;
128
129
				case 'kirki-dimensions':
130
					_.each( value, function( subValue, id ) {
131
						jQuery( $this.findElement( setting, '.' + id + ' input' ) ).prop( 'value', subValue );
132
					});
133
					break;
134
135
				case 'kirki-repeater':
136
137
					// Not yet implemented.
138
					break;
139
140
				case 'kirki-custom':
141
142
					// Do nothing.
143
					break;
144
				default:
145
					jQuery( $this.findElement( setting, 'input' ) ).prop( 'value', value );
146
			}
147
		},
148
149
		/**
150
		 * Set the value for colorpickers.
151
		 * CAUTION: This only sets the value visually, it does not change it in th wp object.
152
		 *
153
		 * @since 3.0.0
154
		 * @param object selector jQuery object for this element.
155
		 * @param string value    The value we want to set.
156
		 */
157
		setColorPicker: function( selector, value ) {
158
			selector.attr( 'data-default-color', value ).data( 'default-color', value ).wpColorPicker( 'color', value );
159
		},
160
161
		/**
162
		 * Sets the value in a selectWoo element.
163
		 * CAUTION: This only sets the value visually, it does not change it in th wp object.
164
		 *
165
		 * @since 3.0.0
166
		 * @param string selector The CSS identifier for this selectWoo.
167
		 * @param string value    The value we want to set.
168
		 */
169
		setSelectWoo: function( selector, value ) {
170
			jQuery( selector ).selectWoo().val( value ).trigger( 'change' );
171
		},
172
173
		/**
174
		 * Sets the value in textarea elements.
175
		 * CAUTION: This only sets the value visually, it does not change it in th wp object.
176
		 *
177
		 * @since 3.0.0
178
		 * @param string selector The CSS identifier for this textarea.
179
		 * @param string value    The value we want to set.
180
		 */
181
		setTextarea: function( selector, value ) {
182
			jQuery( selector ).prop( 'value', value );
183
		},
184
185
		/**
186
		 * Finds an element inside this control.
187
		 *
188
		 * @since 3.0.0
189
		 * @param string setting The setting ID.
190
		 * @param string element The CSS identifier.
191
		 */
192
		findElement: function( setting, element ) {
193
			return wp.customize.control( setting ).container.find( element );
194
		},
195
196
		/**
197
		 * Updates the value in the wp.customize object.
198
		 *
199
		 * @since 3.0.0
200
		 * @param string setting The setting-ID.
201
		 * @param mixed  value   The value.
202
		 */
203
		setValue: function( setting, value, timeout ) {
204
			timeout = ( _.isUndefined( timeout ) ) ? 100 : parseInt( timeout, 10 );
205
			wp.customize.instance( setting ).set({});
206
			setTimeout( function() {
207
				wp.customize.instance( setting ).set( value );
208
			}, timeout );
209
		}
210
	};
211
}
212
var kirki = {
213
214
	initialized: false,
215
216
	/**
217
	 * Initialize the object.
218
	 *
219
	 * @since 3.0.17
220
	 * @returns {null}
221
	 */
222
	initialize: function() {
223
		var self = this;
224
225
		// We only need to initialize once.
226
		if ( self.initialized ) {
227
			return;
228
		}
229
230
		setTimeout( function() {
231
			kirki.util.webfonts.standard.initialize();
232
			kirki.util.webfonts.google.initialize();
233
		}, 150 );
234
235
		// Mark as initialized.
236
		self.initialized = true;
237
	}
238
};
239
240
// Initialize the kirki object.
241
kirki.initialize();
242
var kirki = kirki || {};
243
kirki = jQuery.extend( kirki, {
244
245
	/**
246
	 * An object containing definitions for controls.
247
	 *
248
	 * @since 3.0.16
249
	 */
250
	control: {
251
252
		/**
253
		 * The radio control.
254
		 *
255
		 * @since 3.0.17
256
		 */
257
		'kirki-radio': {
258
259
			/**
260
			 * Init the control.
261
			 *
262
			 * @since 3.0.17
263
			 * @param {Object} control - The customizer control object.
264
			 * @returns {null}
265
			 */
266
			init: function( control ) {
267
				var self = this;
268
269
				// Render the template.
270
				self.template( control );
271
272
				// Init the control.
273
				kirki.input.radio.init( control );
274
275
			},
276
277
			/**
278
			 * Render the template.
279
			 *
280
			 * @since 3.0.17
281
			 * @param {Object} control - The customizer control object.
282
			 * @param {Object} control.params - The control parameters.
283
			 * @param {string} control.params.label - The control label.
284
			 * @param {string} control.params.description - The control description.
285
			 * @param {string} control.params.inputAttrs - extra input arguments.
286
			 * @param {string} control.params.default - The default value.
287
			 * @param {Object} control.params.choices - Any extra choices we may need.
288
			 * @param {string} control.id - The setting.
289
			 * @returns {null}
290
			 */
291
			template: function( control ) {
292
				var template = wp.template( 'kirki-input-radio' );
293
				control.container.html( template( {
294
					label: control.params.label,
295
					description: control.params.description,
296
					'data-id': control.id,
297
					inputAttrs: control.params.inputAttrs,
298
					'default': control.params['default'],
299
					value: kirki.setting.get( control.id ),
300
					choices: control.params.choices
301
				} ) );
302
			}
303
		},
304
305
		/**
306
		 * The color control.
307
		 *
308
		 * @since 3.0.16
309
		 */
310
		'kirki-color': {
311
312
			/**
313
			 * Init the control.
314
			 *
315
			 * @since 3.0.16
316
			 * @param {Object} control - The customizer control object.
317
			 * @returns {null}
318
			 */
319
			init: function( control ) {
320
				var self = this;
321
322
				// Render the template.
323
				self.template( control );
324
325
				// Init the control.
326
				kirki.input.color.init( control );
327
328
			},
329
330
			/**
331
			 * Render the template.
332
			 *
333
			 * @since 3.0.16
334
			 * @param {Object}     control - The customizer control object.
335
			 * @param {Object}     control.params - The control parameters.
336
			 * @param {string}     control.params.label - The control label.
337
			 * @param {string}     control.params.description - The control description.
338
			 * @param {string}     control.params.mode - The colorpicker mode. Can be 'full' or 'hue'.
339
			 * @param {bool|array} control.params.palette - false if we don't want a palette,
340
			 *                                              true to use the default palette,
341
			 *                                              array of custom hex colors if we want a custom palette.
342
			 * @param {string}     control.params.inputAttrs - extra input arguments.
343
			 * @param {string}     control.params.default - The default value.
344
			 * @param {Object}     control.params.choices - Any extra choices we may need.
345
			 * @param {boolean}    control.params.choices.alpha - should we add an alpha channel?
346
			 * @param {string}     control.id - The setting.
347
			 * @returns {null}
348
			 */
349
			template: function( control ) {
350
				var template = wp.template( 'kirki-input-color' );
351
				control.container.html( template( {
352
					label: control.params.label,
353
					description: control.params.description,
354
					'data-id': control.id,
355
					mode: control.params.mode,
356
					inputAttrs: control.params.inputAttrs,
357
					'data-palette': control.params.palette,
358
					'data-default-color': control.params['default'],
359
					'data-alpha': control.params.choices.alpha,
360
					value: kirki.setting.get( control.id )
361
				} ) );
362
			}
363
		},
364
365
		/**
366
		 * The generic control.
367
		 *
368
		 * @since 3.0.16
369
		 */
370
		'kirki-generic': {
371
372
			/**
373
			 * Init the control.
374
			 *
375
			 * @since 3.0.17
376
			 * @param {Object} control - The customizer control object.
377
			 * @param {Object} control.params - Control parameters.
378
			 * @param {Object} control.params.choices - Define the specifics for this input.
379
			 * @param {string} control.params.choices.element - The HTML element we want to use ('input', 'div', 'span' etc).
380
			 * @returns {null}
381
			 */
382
			init: function( control ) {
383
				var self = this;
384
385
				// Render the template.
386
				self.template( control );
387
388
				// Init the control.
389
				if ( ! _.isUndefined( control.params ) && ! _.isUndefined( control.params.choices ) && ! _.isUndefined( control.params.choices.element ) && 'textarea' === control.params.choices.element ) {
390
					kirki.input.textarea.init( control );
391
					return;
392
				}
393
				kirki.input.genericInput.init( control );
394
			},
395
396
			/**
397
			 * Render the template.
398
			 *
399
			 * @since 3.0.17
400
			 * @param {Object}  control - The customizer control object.
401
			 * @param {Object}  control.params - The control parameters.
402
			 * @param {string}  control.params.label - The control label.
403
			 * @param {string}  control.params.description - The control description.
404
			 * @param {string}  control.params.inputAttrs - extra input arguments.
405
			 * @param {string}  control.params.default - The default value.
406
			 * @param {Object}  control.params.choices - Any extra choices we may need.
407
			 * @param {boolean} control.params.choices.alpha - should we add an alpha channel?
408
			 * @param {string}  control.id - The setting.
409
			 * @returns {null}
410
			 */
411
			template: function( control ) {
412
				var args = {
413
						label: control.params.label,
414
						description: control.params.description,
415
						'data-id': control.id,
416
						inputAttrs: control.params.inputAttrs,
417
						choices: control.params.choices,
418
						value: kirki.setting.get( control.id )
419
				    },
420
				    template;
421
422
				if ( ! _.isUndefined( control.params ) && ! _.isUndefined( control.params.choices ) && ! _.isUndefined( control.params.choices.element ) && 'textarea' === control.params.choices.element ) {
423
					template = wp.template( 'kirki-input-textarea' );
424
					control.container.html( template( args ) );
425
					return;
426
				}
427
				template = wp.template( 'kirki-input-generic' );
428
				control.container.html( template( args ) );
429
			}
430
		},
431
432
		'kirki-select': {
433
434
			/**
435
			 * Init the control.
436
			 *
437
			 * @since 3.0.17
438
			 * @param {Object} control - The customizer control object.
439
			 * @returns {null}
440
			 */
441
			init: function( control ) {
442
				var self = this;
443
444
				// Render the template.
445
				self.template( control );
446
447
				// Init the control.
448
				kirki.input.select.init( control );
449
			},
450
451
			/**
452
			 * Render the template.
453
			 *
454
			 * @since 3.0.17
455
			 * @param {Object}  control - The customizer control object.
456
			 * @param {Object}  control.params - The control parameters.
457
			 * @param {string}  control.params.label - The control label.
458
			 * @param {string}  control.params.description - The control description.
459
			 * @param {string}  control.params.inputAttrs - extra input arguments.
460
			 * @param {Object}  control.params.default - The default value.
461
			 * @param {Object}  control.params.choices - The choices for the select dropdown.
462
			 * @param {string}  control.id - The setting.
463
			 * @returns {null}
464
			 */
465
			template: function( control ) {
466
				var template = wp.template( 'kirki-input-select' );
467
468
				control.container.html( template( {
469
					label: control.params.label,
470
					description: control.params.description,
471
					'data-id': control.id,
472
					inputAttrs: control.params.inputAttrs,
473
					choices: control.params.choices,
474
					value: kirki.setting.get( control.id ),
475
					multiple: control.params.multiple || 1,
476
					placeholder: control.params.placeholder
477
			    } ) );
478
			}
479
		}
480
	}
481
} );
482
/* global kirkiL10n */
483
var kirki = kirki || {};
484
kirki = jQuery.extend( kirki, {
485
	/**
486
	 * An object containing definitions for input fields.
487
	 *
488
	 * @since 3.0.16
489
	 */
490
	input: {
491
492
		/**
493
		 * Radio input fields.
494
		 *
495
		 * @since 3.0.17
496
		 */
497
		radio: {
498
499
			/**
500
			 * Init the control.
501
			 *
502
			 * @since 3.0.17
503
			 * @param {Object} control - The control object.
504
			 * @param {Object} control.id - The setting.
505
			 * @returns {null}
506
			 */
507
			init: function( control ) {
508
				var input = jQuery( 'input[data-id="' + control.id + '"]' );
509
510
				// Save the value
511
				input.on( 'change keyup paste click', function() {
512
					kirki.setting.set( control.id, jQuery( this ).val() );
513
				});
514
			}
515
		},
516
517
		/**
518
		 * Color input fields.
519
		 *
520
		 * @since 3.0.16
521
		 */
522
		color: {
523
524
			/**
525
			 * Init the control.
526
			 *
527
			 * @since 3.0.16
528
			 * @param {Object} control - The control object.
529
			 * @param {Object} control.id - The setting.
530
			 * @param {Object} control.choices - Additional options for the colorpickers.
531
			 * @param {Object} control.params - Control parameters.
532
			 * @param {Object} control.params.choices - alias for control.choices.
533
534
			 * @returns {null}
535
			 */
536
			init: function( control ) {
537
				var picker = jQuery( '.kirki-color-control[data-id="' + control.id + '"]' ),
538
				    clear;
539
540
				control.choices = control.choices || {};
541
				if ( _.isEmpty( control.choices ) && control.params.choices ) {
542
					control.choices = control.params.choices;
543
				}
544
545
				// If we have defined any extra choices, make sure they are passed-on to Iris.
546
				if ( ! _.isEmpty( control.choices ) ) {
547
					picker.wpColorPicker( control.choices );
548
				}
549
550
				// Tweaks to make the "clear" buttons work.
551
				setTimeout( function() {
552
					clear = jQuery( '.kirki-input-container[data-id="' + control.id + '"] .wp-picker-clear' );
553
					if ( clear.length ) {
554
						clear.click( function() {
555
							kirki.setting.set( control.id, '' );
556
						});
557
					}
558
				}, 200 );
559
560
				// Saves our settings to the WP API
561
				picker.wpColorPicker({
562
					change: function() {
563
564
						// Small hack: the picker needs a small delay
565
						setTimeout( function() {
566
							kirki.setting.set( control.id, picker.val() );
567
						}, 20 );
568
					}
569
				});
570
			}
571
		},
572
573
		/**
574
		 * Generic input fields.
575
		 *
576
		 * @since 3.0.17
577
		 */
578
		genericInput: {
579
580
			/**
581
			 * Init the control.
582
			 *
583
			 * @since 3.0.17
584
			 * @param {Object} control - The control object.
585
			 * @param {Object} control.id - The setting.
586
			 * @returns {null}
587
			 */
588
			init: function( control ) {
589
				var input = jQuery( 'input[data-id="' + control.id + '"]' );
590
591
				// Save the value
592
				input.on( 'change keyup paste click', function() {
593
					kirki.setting.set( control.id, jQuery( this ).val() );
594
				});
595
			}
596
		},
597
598
		/**
599
		 * Generic input fields.
600
		 *
601
		 * @since 3.0.17
602
		 */
603
		textarea: {
604
605
			/**
606
			 * Init the control.
607
			 *
608
			 * @since 3.0.17
609
			 * @param {Object} control - The control object.
610
			 * @param {Object} control.id - The setting.
611
			 * @returns {null}
612
			 */
613
			init: function( control ) {
614
				var textarea = jQuery( 'textarea[data-id="' + control.id + '"]' );
615
616
				// Save the value
617
				textarea.on( 'change keyup paste click', function() {
618
					kirki.setting.set( control.id, jQuery( this ).val() );
619
				});
620
			}
621
		},
622
623
		select: {
624
625
			/**
626
			 * Init the control.
627
			 *
628
			 * @since 3.0.17
629
			 * @param {Object} control - The control object.
630
			 * @param {Object} control.id - The setting.
631
			 * @returns {null}
632
			 */
633
			init: function( control ) {
634
				var element  = jQuery( 'select[data-id="' + control.id + '"]' ),
635
				    multiple = parseInt( element.data( 'multiple' ), 10 ),
636
				    selectValue,
637
				    selectWooOptions = {
638
						escapeMarkup: function( markup ) {
639
							return markup;
640
						}
641
				    };
642
					if ( control.params.placeholder ) {
643
						selectWooOptions.placeholder = control.params.placeholder;
644
						selectWooOptions.allowClear = true;
645
					}
646
647
				if ( 1 < multiple ) {
648
					selectWooOptions.maximumSelectionLength = multiple;
649
				}
650
				jQuery( element ).selectWoo( selectWooOptions ).on( 'change', function() {
651
					selectValue = jQuery( this ).val();
652
					selectValue = ( null === selectValue && 1 < multiple ) ? [] : selectValue;
653
					kirki.setting.set( control.id, selectValue );
654
				});
655
			}
656
		},
657
658
		image: {
659
660
			/**
661
			 * Get the HTML for image inputs.
662
			 *
663
			 * @since 3.0.17
664
			 * @param {Object} data - The arguments.
665
			 * @returns {string}
666
			 */
667
			getTemplate: function( data ) {
668
				var html   = '',
669
				    saveAs = 'url',
670
				    url;
671
672
				data = _.defaults( data, {
673
					label: '',
674
					description: '',
675
					inputAttrs: '',
676
					'data-id': '',
677
					choices: {},
678
					value: ''
679
				} );
680
681
				if ( ! _.isUndefined( data.choices ) && ! _.isUndefined( data.choices.save_as ) ) {
682
					saveAs = data.choices.save_as;
683
				}
684
				url = data.value;
685
				if ( _.isObject( data.value ) && ! _.isUndefined( data.value.url ) ) {
686
					url = data.value.url;
687
				}
688
689
				html += '<label>';
690
				if ( data.label ) {
691
					html += '<span class="customize-control-title">' + data.label + '</span>';
692
				}
693
				if ( data.description ) {
694
					html += '<span class="description customize-control-description">' + data.description + '</span>';
695
				}
696
				html += '</label>';
697
				html += '<div class="image-wrapper attachment-media-view image-upload">';
698
				if ( data.value.url || '' !== url ) {
699
					html += '<div class="thumbnail thumbnail-image"><img src="' + url + '" alt="" /></div>';
700
				} else {
701
					html += '<div class="placeholder">' + kirkiL10n.noFileSelected + '</div>';
702
				}
703
				html += '<div class="actions">';
704
				html += '<button class="button image-upload-remove-button' + ( '' === url ? ' hidden' : '' ) + '">' + kirkiL10n.remove + '</button>';
705
				if ( data['default'] && '' !== data['default'] ) {
706
					html += '<button type="button" class="button image-default-button"';
707
					if ( data['default'] === data.value || ( ! _.isUndefined( data.value.url ) && data['default'] === data.value.url ) ) {
708
						html += ' style="display:none;"';
709
					}
710
					html += '>' + kirkiL10n['default'] + '</button>';
711
				}
712
				html += '<button type="button" class="button image-upload-button">' + kirkiL10n.selectFile + '</button>';
713
				html += '</div></div>';
714
715
				return '<div class="kirki-input-container" data-id="' + data.id + '">' + html + '</div>';
716
			},
717
718
			/**
719
			 * Init the control.
720
			 *
721
			 * @since 3.0.17
722
			 * @param {Object} control - The control object.
723
			 * @returns {null}
724
			 */
725
			init: function( control ) { // jshint ignore:line
726
			}
727
		}
728
	}
729
} );
730
var kirki = kirki || {};
731
kirki = jQuery.extend( kirki, {
732
	/**
733
	 * An object containing definitions for settings.
734
	 *
735
	 * @since 3.0.16
736
	 */
737
	setting: {
738
739
		/**
740
		 * Gets the value of a setting.
741
		 *
742
		 * This is a helper function that allows us to get the value of
743
		 * control[key1][key2] for example, when the setting used in the
744
		 * customizer API is "control".
745
		 *
746
		 * @since 3.0.16
747
		 * @param {string} setting - The setting for which we're getting the value.
748
		 * @returns {mixed} Depends on the value.
749
		 */
750
		get: function( setting ) {
751
			var parts        = setting.split( '[' ),
752
			    foundSetting = '',
753
			    foundInStep  = 0,
754
			    currentVal   = '';
755
756
			_.each( parts, function( part, i ) {
757
				part = part.replace( ']', '' );
758
759
				if ( 0 === i ) {
760
					foundSetting = part;
761
				} else {
762
					foundSetting += '[' + part + ']';
763
				}
764
765
				if ( ! _.isUndefined( wp.customize.instance( foundSetting ) ) ) {
766
					currentVal  = wp.customize.instance( foundSetting ).get();
767
					foundInStep = i;
768
				}
769
770
				if ( foundInStep < i ) {
771
					if ( _.isObject( currentVal ) && ! _.isUndefined( currentVal[ part ] ) ) {
772
						currentVal = currentVal[ part ];
773
					}
774
				}
775
			});
776
777
			return currentVal;
778
		},
779
780
		/**
781
		 * Sets the value of a setting.
782
		 *
783
		 * This function is a bit complicated because there any many scenarios to consider.
784
		 * Example: We want to save the value for my_setting[something][3][something-else].
785
		 * The control's setting is my_setting[something].
786
		 * So we need to find that first, then figure out the remaining parts,
787
		 * merge the values recursively to avoid destroying my_setting[something][2]
788
		 * and also take into account any defined "key" arguments which take this even deeper.
789
		 *
790
		 * @since 3.0.16
791
		 * @param {object|string} element - The DOM element whose value has changed,
792
		 *                                  or an ID.
793
		 * @param {mixed}         value - Depends on the control-type.
794
		 * @param {string}        key - If we only want to save an item in an object
795
		 *                                  we can define the key here.
796
		 * @returns {null}
797
		 */
798
		set: function( element, value, key ) {
799
			var setting,
800
			    parts,
801
			    currentNode   = '',
802
			    foundNode     = '',
803
			    subSettingObj = {},
804
			    currentVal,
805
			    subSetting,
806
			    subSettingParts;
807
808
			// Get the setting from the element.
809
			setting = element;
810
			if ( _.isObject( element ) ) {
811
				if ( jQuery( element ).attr( 'data-id' ) ) {
812
					setting = element.attr( 'data-id' );
813
				} else {
814
					setting = element.parents( '[data-id]' ).attr( 'data-id' );
815
				}
816
			}
817
818
			if ( 'undefined' !== typeof wp.customize.control( setting ) ) {
819
				wp.customize.control( setting ).setting.set( value );
820
				return;
821
			}
822
823
			parts = setting.split( '[' ),
824
825
			// Find the setting we're using in the control using the customizer API.
826
			_.each( parts, function( part, i ) {
827
				part = part.replace( ']', '' );
828
829
				// The current part of the setting.
830
				currentNode = ( 0 === i ) ? part : '[' + part + ']';
831
832
				// When we find the node, get the value from it.
833
				// In case of an object we'll need to merge with current values.
834
				if ( ! _.isUndefined( wp.customize.instance( currentNode ) ) ) {
835
					foundNode  = currentNode;
836
					currentVal = wp.customize.instance( foundNode ).get();
837
				}
838
			} );
839
840
			// Get the remaining part of the setting that was unused.
841
			subSetting = setting.replace( foundNode, '' );
842
843
			// If subSetting is not empty, then we're dealing with an object
844
			// and we need to dig deeper and recursively merge the values.
845
			if ( '' !== subSetting ) {
846
				if ( ! _.isObject( currentVal ) ) {
847
					currentVal = {};
848
				}
849
				if ( '[' === subSetting.charAt( 0 ) ) {
850
					subSetting = subSetting.replace( '[', '' );
851
				}
852
				subSettingParts = subSetting.split( '[' );
853
				_.each( subSettingParts, function( subSettingPart, i ) {
854
					subSettingParts[ i ] = subSettingPart.replace( ']', '' );
855
				} );
856
857
				// If using a key, we need to go 1 level deeper.
858
				if ( key ) {
859
					subSettingParts.push( key );
860
				}
861
862
				// Converting to a JSON string and then parsing that to an object
863
				// may seem a bit hacky and crude but it's efficient and works.
864
				subSettingObj = '{"' + subSettingParts.join( '":{"' ) + '":"' + value + '"' + '}'.repeat( subSettingParts.length );
865
				subSettingObj = JSON.parse( subSettingObj );
866
867
				// Recursively merge with current value.
868
				jQuery.extend( true, currentVal, subSettingObj );
869
				value = currentVal;
870
871
			} else {
872
				if ( key ) {
873
					currentVal = ( ! _.isObject( currentVal ) ) ? {} : currentVal;
874
					currentVal[ key ] = value;
875
					value = currentVal;
876
				}
877
			}
878
			wp.customize.control( foundNode ).setting.set( value );
879
		}
880
	}
881
} );
882
/* global ajaxurl */
883
var kirki = kirki || {};
884
kirki = jQuery.extend( kirki, {
885
	/**
886
	 * A collection of utility methods.
887
	 *
888
	 * @since 3.0.17
889
	 */
890
	util: {
891
892
		/**
893
		 * A collection of utility methods for webfonts.
894
		 *
895
		 * @since 3.0.17
896
		 */
897
		webfonts: {
898
899
			/**
900
			 * Google-fonts related methods.
901
			 *
902
			 * @since 3.0.17
903
			 */
904
			google: {
905
906
				/**
907
				 * An object containing all Google fonts.
908
				 *
909
				 * to set this call this.setFonts();
910
				 *
911
				 * @since 3.0.17
912
				 */
913
				fonts: {},
914
915
				/**
916
				 * Init for google-fonts.
917
				 *
918
				 * @since 3.0.17
919
				 * @returns {null}
920
				 */
921
				initialize: function() {
922
					var self = this;
923
924
					self.setFonts();
925
				},
926
927
				/**
928
				 * Set fonts in this.fonts
929
				 *
930
				 * @since 3.0.17
931
				 * @returns {null}
932
				 */
933
				setFonts: function() {
934
					var self = this;
935
936
					// No need to run if we already have the fonts.
937
					if ( ! _.isEmpty( self.fonts ) ) {
938
						return;
939
					}
940
941
					// Make an AJAX call to set the fonts object (alpha).
942
					jQuery.post( ajaxurl, { 'action': 'kirki_fonts_google_all_get' }, function( response ) {
943
944
						// Get fonts from the JSON array.
945
						self.fonts = JSON.parse( response );
946
					} );
947
				},
948
949
				/**
950
				 * Gets all properties of a font-family.
951
				 *
952
				 * @since 3.0.17
953
				 * @param {string} family - The font-family we're interested in.
954
				 * @returns {Object}
955
				 */
956
				getFont: function( family ) {
957
					var self = this,
958
					    fonts = self.getFonts();
959
960
					if ( 'undefined' === typeof fonts[ family ] ) {
961
						return false;
962
					}
963
					return fonts[ family ];
964
				},
965
966
				/**
967
				 * Gets all properties of a font-family.
968
				 *
969
				 * @since 3.0.17
970
				 * @param {string} order - How to order the fonts (alpha|popularity|trending).
971
				 * @param {int}    number - How many to get. 0 for all.
972
				 * @returns {Object}
973
				 */
974
				getFonts: function( order, number ) {
975
					var self    = this,
976
					    ordered = {},
977
					    partial = [];
978
979
					// Make sure order is correct.
980
					order  = order || 'alpha';
981
					order  = ( 'alpha' !== order && 'popularity' !== order && 'trending' !== order ) ? 'alpha' : order;
982
983
					// Make sure number is correct.
984
					number = number || 0;
985
					number = parseInt( number, 10 );
986
987
					if ( 'alpha' === order || 0 === number ) {
988
						ordered = self.fonts.items;
989
					} else {
990
						partial = _.first( self.fonts.order[ order ], number );
991
						_.each( partial, function( family ) {
992
							ordered[ family ] = self.fonts.items[ family ];
993
						} );
994
					}
995
996
					return ordered;
997
				},
998
999
				/**
1000
				 * Gets the variants for a font-family.
1001
				 *
1002
				 * @since 3.0.17
1003
				 * @param {string} family - The font-family we're interested in.
1004
				 * @returns {Array}
1005
				 */
1006
				getVariants: function( family ) {
1007
					var self = this,
1008
					    font = self.getFont( family );
1009
1010
					// Early exit if font was not found.
1011
					if ( ! font ) {
1012
						return false;
1013
					}
1014
1015
					// Early exit if font doesn't have variants.
1016
					if ( _.isUndefined( font.variants ) ) {
1017
						return false;
1018
					}
1019
1020
					// Return the variants.
1021
					return font.variants;
1022
				},
1023
1024
				/**
1025
				 * Get the subsets for a font-family.
1026
				 *
1027
				 * @since 3.0.17
1028
				 * @param {string} family - The font-family we're interested in.
1029
				 * @returns {Object}
1030
				 */
1031
				getSubsets: function( family ) {
1032
					var self = this,
1033
					    font = self.getFont( family );
1034
1035
					// Early exit if font was not found.
1036
					if ( ! font ) {
1037
						return false;
1038
					}
1039
1040
					// Early exit if font doesn't have subsets.
1041
					if ( _.isUndefined( font.subsets ) ) {
1042
						return false;
1043
					}
1044
1045
					// Return the variants.
1046
					return font.subsets;
1047
				}
1048
			},
1049
1050
			/**
1051
			 * Standard fonts related methods.
1052
			 *
1053
			 * @since 3.0.17
1054
			 */
1055
			standard: {
1056
1057
				/**
1058
				 * An object containing all Standard fonts.
1059
				 *
1060
				 * to set this call this.setFonts();
1061
				 *
1062
				 * @since 3.0.17
1063
				 */
1064
				fonts: {},
1065
1066
				/**
1067
				 * Init for google-fonts.
1068
				 *
1069
				 * @since 3.0.17
1070
				 * @returns {null}
1071
				 */
1072
				initialize: function() {
1073
					var self = this;
1074
1075
					self.setFonts();
1076
				},
1077
1078
				/**
1079
				 * Set fonts in this.fonts
1080
				 *
1081
				 * @since 3.0.17
1082
				 * @returns {null}
1083
				 */
1084
				setFonts: function() {
1085
					var self = this;
1086
1087
					// No need to run if we already have the fonts.
1088
					if ( ! _.isEmpty( self.fonts ) ) {
1089
						return;
1090
					}
1091
1092
					// Make an AJAX call to set the fonts object.
1093
					jQuery.post( ajaxurl, { 'action': 'kirki_fonts_standard_all_get' }, function( response ) {
1094
1095
						// Get fonts from the JSON array.
1096
						self.fonts = JSON.parse( response );
1097
					} );
1098
				},
1099
1100
				/**
1101
				 * Gets the variants for a font-family.
1102
				 *
1103
				 * @since 3.0.17
1104
				 * @returns {Array}
1105
				 */
1106
				getVariants: function( family ) { // jshint ignore: line
1107
					return ['regular', 'italic', '700', '700italic'];
1108
				}
1109
			},
1110
1111
			/**
1112
			 * Figure out what this font-family is (google/standard)
1113
			 *
1114
			 * @since 3.0.20
1115
			 * @param {string} family - The font-family.
1116
			 * @returns {string|false} - Returns string if found (google|standard)
1117
			 *                           and false in case the font-family is an arbitrary value
1118
			 *                           not found anywhere in our font definitions.
1119
			 */
1120
			getFontType: function( family ) {
1121
				var self = this;
1122
1123
				// Check for standard fonts first.
1124
				if (
1125
					'undefined' !== typeof self.standard.fonts[ family ] || (
1126
						'undefined' !== typeof self.standard.fonts.stack &&
1127
						'undefined' !== typeof self.standard.fonts.stack[ family ]
1128
					)
1129
				) {
1130
					return 'standard';
1131
				}
1132
1133
				// Check in googlefonts.
1134
				if ( 'undefined' !== typeof self.google.fonts.items[ family ] ) {
1135
					return 'google';
1136
				}
1137
				return false;
1138
			}
1139
		}
1140
	}
1141
} );
1142
/* global kirki */
1143
/**
1144
 * The majority of the code in this file
1145
 * is derived from the wp-customize-posts plugin
1146
 * and the work of @westonruter to whom I am very grateful.
1147
 *
1148
 * @see https://github.com/xwp/wp-customize-posts
1149
 */
1150
1151
( function() {
1152
	'use strict';
1153
1154
	/**
1155
	 * A dynamic color-alpha control.
1156
	 *
1157
	 * @class
1158
	 * @augments wp.customize.Control
1159
	 * @augments wp.customize.Class
1160
	 */
1161
	wp.customize.kirkiDynamicControl = wp.customize.Control.extend({
1162
1163
		initialize: function( id, options ) {
1164
			var control = this,
1165
			    args    = options || {};
1166
1167
			args.params = args.params || {};
1168
			if ( ! args.params.type ) {
1169
				args.params.type = 'kirki-generic';
1170
			}
1171
			if ( ! args.params.content ) {
1172
				args.params.content = jQuery( '<li></li>' );
1173
				args.params.content.attr( 'id', 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ) );
1174
				args.params.content.attr( 'class', 'customize-control customize-control-' + args.params.type );
1175
			}
1176
1177
			control.propertyElements = [];
1178
			wp.customize.Control.prototype.initialize.call( control, id, args );
1179
		},
1180
1181
		/**
1182
		 * Add bidirectional data binding links between inputs and the setting(s).
1183
		 *
1184
		 * This is copied from wp.customize.Control.prototype.initialize(). It
1185
		 * should be changed in Core to be applied once the control is embedded.
1186
		 *
1187
		 * @private
1188
		 * @returns {null}
1189
		 */
1190
		_setUpSettingRootLinks: function() {
1191
			var control = this,
1192
			    nodes   = control.container.find( '[data-customize-setting-link]' );
1193
1194
			nodes.each( function() {
1195
				var node = jQuery( this );
1196
1197
				wp.customize( node.data( 'customizeSettingLink' ), function( setting ) {
1198
					var element = new wp.customize.Element( node );
1199
					control.elements.push( element );
1200
					element.sync( setting );
1201
					element.set( setting() );
1202
				});
1203
			});
1204
		},
1205
1206
		/**
1207
		 * Add bidirectional data binding links between inputs and the setting properties.
1208
		 *
1209
		 * @private
1210
		 * @returns {null}
1211
		 */
1212
		_setUpSettingPropertyLinks: function() {
1213
			var control = this,
1214
			    nodes;
1215
1216
			if ( ! control.setting ) {
1217
				return;
1218
			}
1219
1220
			nodes = control.container.find( '[data-customize-setting-property-link]' );
1221
1222
			nodes.each( function() {
1223
				var node = jQuery( this ),
1224
				    element,
1225
				    propertyName = node.data( 'customizeSettingPropertyLink' );
1226
1227
				element = new wp.customize.Element( node );
1228
				control.propertyElements.push( element );
1229
				element.set( control.setting()[ propertyName ] );
1230
1231
				element.bind( function( newPropertyValue ) {
1232
					var newSetting = control.setting();
1233
					if ( newPropertyValue === newSetting[ propertyName ] ) {
1234
						return;
1235
					}
1236
					newSetting = _.clone( newSetting );
1237
					newSetting[ propertyName ] = newPropertyValue;
1238
					control.setting.set( newSetting );
1239
				} );
1240
				control.setting.bind( function( newValue ) {
1241
					if ( newValue[ propertyName ] !== element.get() ) {
1242
						element.set( newValue[ propertyName ] );
1243
					}
1244
				} );
1245
			});
1246
		},
1247
1248
		/**
1249
		 * @inheritdoc
1250
		 */
1251
		ready: function() {
1252
			var control = this;
1253
1254
			control._setUpSettingRootLinks();
1255
			control._setUpSettingPropertyLinks();
1256
1257
			wp.customize.Control.prototype.ready.call( control );
1258
1259
			control.deferred.embedded.done( function() {
1260
				control.initKirkiControl( control );
1261
			});
1262
		},
1263
1264
		/**
1265
		 * Embed the control in the document.
1266
		 *
1267
		 * Override the embed() method to do nothing,
1268
		 * so that the control isn't embedded on load,
1269
		 * unless the containing section is already expanded.
1270
		 *
1271
		 * @returns {null}
1272
		 */
1273
		embed: function() {
1274
			var control   = this,
1275
			    sectionId = control.section();
1276
1277
			if ( ! sectionId ) {
1278
				return;
1279
			}
1280
1281
			wp.customize.section( sectionId, function( section ) {
1282
				if ( 'kirki-expanded' === section.params.type || section.expanded() || wp.customize.settings.autofocus.control === control.id ) {
1283
					control.actuallyEmbed();
1284
				} else {
1285
					section.expanded.bind( function( expanded ) {
1286
						if ( expanded ) {
1287
							control.actuallyEmbed();
1288
						}
1289
					} );
1290
				}
1291
			} );
1292
		},
1293
1294
		/**
1295
		 * Deferred embedding of control when actually
1296
		 *
1297
		 * This function is called in Section.onChangeExpanded() so the control
1298
		 * will only get embedded when the Section is first expanded.
1299
		 *
1300
		 * @returns {null}
1301
		 */
1302
		actuallyEmbed: function() {
1303
			var control = this;
1304
			if ( 'resolved' === control.deferred.embedded.state() ) {
1305
				return;
1306
			}
1307
			control.renderContent();
1308
			control.deferred.embedded.resolve(); // This triggers control.ready().
1309
		},
1310
1311
		/**
1312
		 * This is not working with autofocus.
1313
		 *
1314
		 * @param {object} [args] Args.
1315
		 * @returns {null}
1316
		 */
1317
		focus: function( args ) {
1318
			var control = this;
1319
			control.actuallyEmbed();
1320
			wp.customize.Control.prototype.focus.call( control, args );
1321
		},
1322
1323
		/**
1324
		 * Additional actions that run on ready.
1325
		 *
1326
		 * @param {object} [args] Args.
1327
		 * @returns {null}
1328
		 */
1329
		initKirkiControl: function( control ) {
1330
			if ( 'undefined' !== typeof kirki.control[ control.params.type ] ) {
1331
				kirki.control[ control.params.type ].init( control );
1332
				return;
1333
			}
1334
1335
			// Save the value
1336
			this.container.on( 'change keyup paste click', 'input', function() {
1337
				control.setting.set( jQuery( this ).val() );
1338
			});
1339
		},
1340
1341
		kirkiValidateCSSValue: function( value ) {
1342
1343
			var validUnits = ['rem', 'em', 'ex', '%', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'ch', 'vh', 'vw', 'vmin', 'vmax'],
1344
				numericValue,
1345
				unit;
1346
1347
			// 0 is always a valid value, and we can't check calc() values effectively.
1348
			if ( '0' === value || ( 0 <= value.indexOf( 'calc(' ) && 0 <= value.indexOf( ')' ) ) ) {
1349
				return true;
1350
			}
1351
1352
			if ( 'auto' === value || 'inherit' === value || 'initial' === value ) {
1353
				return true;
1354
			}
1355
1356
			// Get the numeric value.
1357
			numericValue = parseFloat( value );
1358
1359
			// Get the unit
1360
			unit = value.replace( numericValue, '' );
1361
1362
			// Check the validity of the numeric value and units.
1363
			if ( isNaN( numericValue ) || -1 === jQuery.inArray( unit, validUnits ) ) {
1364
				return false;
1365
			}
1366
			return true;
1367
		}
1368
	});
1369
})();
1370
1371
_.each( kirki.control, function( obj, type ) {
1372
	wp.customize.controlConstructor[ type ] = wp.customize.kirkiDynamicControl.extend({});
1373
} );
1374
/* global kirkiControlLoader */
1375
wp.customize.controlConstructor['kirki-background'] = wp.customize.Control.extend({
1376
1377
	// When we're finished loading continue processing
1378
	ready: function() {
1379
1380
		'use strict';
1381
1382
		var control = this;
1383
1384
		// Init the control.
1385
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
1386
			kirkiControlLoader( control );
1387
		} else {
1388
			control.initKirkiControl();
1389
		}
1390
	},
1391
1392
	initKirkiControl: function() {
1393
1394
		var control = this,
1395
		    value   = control.getValue(),
1396
		    picker  = control.container.find( '.kirki-color-control' );
1397
1398
		// Hide unnecessary controls if the value doesn't have an image.
1399
		if ( _.isUndefined( value['background-image'] ) || '' === value['background-image'] ) {
1400
			control.container.find( '.background-wrapper > .background-repeat' ).hide();
1401
			control.container.find( '.background-wrapper > .background-position' ).hide();
1402
			control.container.find( '.background-wrapper > .background-size' ).hide();
1403
			control.container.find( '.background-wrapper > .background-attachment' ).hide();
1404
		}
1405
1406
		// Color.
1407
		picker.wpColorPicker({
1408
			change: function() {
1409
				setTimeout( function() {
1410
					control.saveValue( 'background-color', picker.val() );
1411
				}, 100 );
1412
			}
1413
		});
1414
1415
		// Background-Repeat.
1416
		control.container.on( 'change', '.background-repeat select', function() {
1417
			control.saveValue( 'background-repeat', jQuery( this ).val() );
1418
		});
1419
1420
		// Background-Size.
1421
		control.container.on( 'change click', '.background-size input', function() {
1422
			control.saveValue( 'background-size', jQuery( this ).val() );
1423
		});
1424
1425
		// Background-Position.
1426
		control.container.on( 'change', '.background-position select', function() {
1427
			control.saveValue( 'background-position', jQuery( this ).val() );
1428
		});
1429
1430
		// Background-Attachment.
1431
		control.container.on( 'change click', '.background-attachment input', function() {
1432
			control.saveValue( 'background-attachment', jQuery( this ).val() );
1433
		});
1434
1435
		// Background-Image.
1436
		control.container.on( 'click', '.background-image-upload-button', function( e ) {
1437
			var image = wp.media({ multiple: false }).open().on( 'select', function() {
1438
1439
				// This will return the selected image from the Media Uploader, the result is an object.
1440
				var uploadedImage = image.state().get( 'selection' ).first(),
1441
				    previewImage   = uploadedImage.toJSON().sizes.full.url,
1442
				    imageUrl,
1443
				    imageID,
1444
				    imageWidth,
1445
				    imageHeight,
1446
				    preview,
1447
				    removeButton;
1448
1449
				if ( ! _.isUndefined( uploadedImage.toJSON().sizes.medium ) ) {
1450
					previewImage = uploadedImage.toJSON().sizes.medium.url;
1451
				} else if ( ! _.isUndefined( uploadedImage.toJSON().sizes.thumbnail ) ) {
1452
					previewImage = uploadedImage.toJSON().sizes.thumbnail.url;
1453
				}
1454
1455
				imageUrl    = uploadedImage.toJSON().sizes.full.url;
1456
				imageID     = uploadedImage.toJSON().id;
1457
				imageWidth  = uploadedImage.toJSON().width;
1458
				imageHeight = uploadedImage.toJSON().height;
1459
1460
				// Show extra controls if the value has an image.
1461
				if ( '' !== imageUrl ) {
1462
					control.container.find( '.background-wrapper > .background-repeat, .background-wrapper > .background-position, .background-wrapper > .background-size, .background-wrapper > .background-attachment' ).show();
1463
				}
1464
1465
				control.saveValue( 'background-image', imageUrl );
1466
				preview      = control.container.find( '.placeholder, .thumbnail' );
1467
				removeButton = control.container.find( '.background-image-upload-remove-button' );
1468
1469
				if ( preview.length ) {
1470
					preview.removeClass().addClass( 'thumbnail thumbnail-image' ).html( '<img src="' + previewImage + '" alt="" />' );
1471
				}
1472
				if ( removeButton.length ) {
1473
					removeButton.show();
1474
				}
1475
		    });
1476
1477
			e.preventDefault();
1478
		});
1479
1480
		control.container.on( 'click', '.background-image-upload-remove-button', function( e ) {
1481
1482
			var preview,
1483
			    removeButton;
1484
1485
			e.preventDefault();
1486
1487
			control.saveValue( 'background-image', '' );
1488
1489
			preview      = control.container.find( '.placeholder, .thumbnail' );
1490
			removeButton = control.container.find( '.background-image-upload-remove-button' );
1491
1492
			// Hide unnecessary controls.
1493
			control.container.find( '.background-wrapper > .background-repeat' ).hide();
1494
			control.container.find( '.background-wrapper > .background-position' ).hide();
1495
			control.container.find( '.background-wrapper > .background-size' ).hide();
1496
			control.container.find( '.background-wrapper > .background-attachment' ).hide();
1497
1498
			if ( preview.length ) {
1499
				preview.removeClass().addClass( 'placeholder' ).html( 'No file selected' );
1500
			}
1501
			if ( removeButton.length ) {
1502
				removeButton.hide();
1503
			}
1504
		});
1505
	},
1506
1507
	/**
1508
	 * Gets the value.
1509
	 */
1510
	getValue: function() {
1511
1512
		var control = this,
1513
		    value   = {};
1514
1515
		// Make sure everything we're going to need exists.
1516
		_.each( control.params['default'], function( defaultParamValue, param ) {
1517
			if ( false !== defaultParamValue ) {
1518
				value[ param ] = defaultParamValue;
1519
				if ( ! _.isUndefined( control.setting._value[ param ] ) ) {
1520
					value[ param ] = control.setting._value[ param ];
1521
				}
1522
			}
1523
		});
1524
		_.each( control.setting._value, function( subValue, param ) {
1525
			if ( _.isUndefined( value[ param ] ) ) {
1526
				value[ param ] = subValue;
1527
			}
1528
		});
1529
		return value;
1530
	},
1531
1532
	/**
1533
	 * Saves the value.
1534
	 */
1535
	saveValue: function( property, value ) {
1536
1537
		'use strict';
1538
1539
		var control   = this,
1540
		    input     = jQuery( '#customize-control-' + control.id.replace( '[', '-' ).replace( ']', '' ) + ' .background-hidden-value' ),
1541
		    valueJSON = jQuery( input ).val(),
1542
		    valueObj  = JSON.parse( valueJSON );
1543
1544
		valueObj[ property ] = value;
1545
		control.setting.set( valueObj );
1546
		jQuery( input ).attr( 'value', JSON.stringify( valueObj ) ).trigger( 'change' );
1547
	}
1548
});
1549
wp.customize.controlConstructor['kirki-color-palette'] = wp.customize.kirkiDynamicControl.extend({});
1550
wp.customize.controlConstructor['kirki-dashicons'] = wp.customize.kirkiDynamicControl.extend({});
1551
wp.customize.controlConstructor['kirki-date'] = wp.customize.kirkiDynamicControl.extend({
1552
1553
	initKirkiControl: function() {
1554
1555
		var control  = this;
1556
1557
		// Only add in WP 4.9+.
1558
		if ( _.isUndefined( wp.customize.DateTimeControl ) ) {
1559
			return;
1560
		}
1561
1562
		// New method for the DateTime control.
1563
		wp.customize.control.add( new wp.customize.DateTimeControl( control.id, {
1564
			section: control.params.section,
1565
			priority: control.params.priority,
1566
			label: control.params.label,
1567
			description: control.params.description,
1568
			settings: { 'default': control.id },
1569
			'default': control.params['default']
1570
		} ) );
1571
	}
1572
});
1573
/* global dimensionkirkiL10n */
1574
wp.customize.controlConstructor['kirki-dimension'] = wp.customize.kirkiDynamicControl.extend({
1575
1576
	initKirkiControl: function() {
1577
1578
		var control = this,
1579
		    value;
1580
1581
		// Notifications.
1582
		control.kirkiNotifications();
1583
1584
		// Save the value
1585
		this.container.on( 'change keyup paste', 'input', function() {
1586
1587
			value = jQuery( this ).val();
1588
			control.setting.set( value );
1589
		});
1590
	},
1591
1592
	/**
1593
	 * Handles notifications.
1594
	 */
1595
	kirkiNotifications: function() {
1596
1597
		var control = this;
1598
1599
		wp.customize( control.id, function( setting ) {
1600
			setting.bind( function( value ) {
1601
				var code = 'long_title';
1602
1603
				if ( false === control.kirkiValidateCSSValue( value ) ) {
1604
					setting.notifications.add( code, new wp.customize.Notification(
1605
						code,
1606
						{
1607
							type: 'warning',
1608
							message: dimensionkirkiL10n['invalid-value']
1609
						}
1610
					) );
1611
				} else {
1612
					setting.notifications.remove( code );
1613
				}
1614
			} );
1615
		} );
1616
	}
1617
});
1618
/* global dimensionskirkiL10n */
1619
wp.customize.controlConstructor['kirki-dimensions'] = wp.customize.kirkiDynamicControl.extend({
1620
1621
	initKirkiControl: function() {
1622
1623
		var control     = this,
1624
		    subControls = control.params.choices.controls,
1625
		    value       = {},
1626
		    subsArray   = [],
1627
		    i;
1628
1629
		_.each( subControls, function( v, i ) {
1630
			if ( true === v ) {
1631
				subsArray.push( i );
1632
			}
1633
		} );
1634
1635
		for ( i = 0; i < subsArray.length; i++ ) {
1636
			value[ subsArray[ i ] ] = control.setting._value[ subsArray[ i ] ];
1637
			control.updateDimensionsValue( subsArray[ i ], value );
1638
		}
1639
	},
1640
1641
	/**
1642
	 * Updates the value.
1643
	 */
1644
	updateDimensionsValue: function( context, value ) {
1645
1646
		var control = this;
1647
1648
		control.container.on( 'change keyup paste', '.' + context + ' input', function() {
1649
			value[ context ] = jQuery( this ).val();
1650
1651
			// Notifications.
1652
			control.kirkiNotifications();
1653
1654
			// Save the value
1655
			control.saveValue( value );
1656
		});
1657
	},
1658
1659
	/**
1660
	 * Saves the value.
1661
	 */
1662
	saveValue: function( value ) {
1663
1664
		var control  = this,
1665
		    newValue = {};
1666
1667
		_.each( value, function( newSubValue, i ) {
1668
			newValue[ i ] = newSubValue;
1669
		});
1670
1671
		control.setting.set( newValue );
1672
	},
1673
1674
	/**
1675
	 * Handles notifications.
1676
	 */
1677
	kirkiNotifications: function() {
1678
1679
		var control = this;
1680
1681
		wp.customize( control.id, function( setting ) {
1682
			setting.bind( function( value ) {
1683
				var code = 'long_title',
1684
				    subs = {},
1685
				    message;
1686
1687
				setting.notifications.remove( code );
1688
1689
				_.each( value, function( val, direction ) {
1690
					if ( false === control.kirkiValidateCSSValue( val ) ) {
1691
						subs[ direction ] = val;
1692
					} else {
1693
						delete subs[ direction ];
1694
					}
1695
				} );
1696
1697
				if ( ! _.isEmpty( subs ) ) {
1698
					message = dimensionskirkiL10n['invalid-value'] + ' (' + _.values( subs ).toString() + ') ';
1699
					setting.notifications.add( code, new wp.customize.Notification( code, {
1700
						type: 'warning',
1701
						message: message
1702
					} ) );
1703
					return;
1704
				}
1705
				setting.notifications.remove( code );
1706
			} );
1707
		} );
1708
	}
1709
});
1710
/* global tinyMCE */
1711
wp.customize.controlConstructor['kirki-editor'] = wp.customize.kirkiDynamicControl.extend({
1712
1713
	initKirkiControl: function() {
1714
1715
		var control = this,
1716
		    element = control.container.find( 'textarea' ),
1717
		    id      = 'kirki-editor-' + control.id.replace( '[', '' ).replace( ']', '' ),
1718
		    editor;
1719
1720
		wp.editor.initialize( id, {
1721
			tinymce: {
1722
				wpautop: true
1723
			},
1724
			quicktags: true,
1725
			mediaButtons: true
1726
		});
1727
1728
		editor = tinyMCE.get( id );
1729
1730
		if ( editor ) {
1731
			editor.onChange.add( function( ed ) {
1732
				var content;
1733
1734
				ed.save();
1735
				content = editor.getContent();
1736
				element.val( content ).trigger( 'change' );
1737
				wp.customize.instance( control.id ).set( content );
1738
			});
1739
		}
1740
	}
1741
});
1742
/* global fontAwesomeJSON */
1743
wp.customize.controlConstructor['kirki-fontawesome'] = wp.customize.kirkiDynamicControl.extend({
1744
1745
	initKirkiControl: function() {
1746
1747
		var control  = this,
1748
		    element  = this.container.find( 'select' ),
1749
		    icons    = jQuery.parseJSON( fontAwesomeJSON ),
1750
		    selectValue,
1751
		    selectWooOptions = {
1752
				data: [],
1753
				escapeMarkup: function( markup ) {
1754
					return markup;
1755
				},
1756
				templateResult: function( val ) {
1757
					return '<i class="fa fa-lg fa-' + val.id + '" aria-hidden="true"></i>' + ' ' + val.text;
1758
				},
1759
				templateSelection: function( val ) {
1760
					return '<i class="fa fa-lg fa-' + val.id + '" aria-hidden="true"></i>' + ' ' + val.text;
1761
				}
1762
		    },
1763
		    select;
1764
1765
		_.each( icons.icons, function( icon ) {
1766
			selectWooOptions.data.push({
1767
				id: icon.id,
1768
				text: icon.name
1769
			});
1770
		});
1771
1772
		select = jQuery( element ).selectWoo( selectWooOptions );
1773
1774
		select.on( 'change', function() {
1775
			selectValue = jQuery( this ).val();
1776
			control.setting.set( selectValue );
1777
		});
1778
		select.val( control.setting._value ).trigger( 'change' );
1779
	}
1780
});
1781
/* global kirkiControlLoader */
1782
wp.customize.controlConstructor['kirki-image'] = wp.customize.Control.extend({
1783
1784
	// When we're finished loading continue processing
1785
	ready: function() {
1786
1787
		'use strict';
1788
1789
		var control = this;
1790
1791
		// Init the control.
1792
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
1793
			kirkiControlLoader( control );
1794
		} else {
1795
			control.initKirkiControl();
1796
		}
1797
	},
1798
1799
	initKirkiControl: function() {
1800
1801
		var control       = this,
1802
		    value         = control.getValue(),
1803
		    saveAs        = ( ! _.isUndefined( control.params.choices ) && ! _.isUndefined( control.params.choices.save_as ) ) ? control.params.choices.save_as : 'url',
1804
		    preview       = control.container.find( '.placeholder, .thumbnail' ),
1805
		    previewImage  = ( 'array' === saveAs ) ? value.url : value,
1806
		    removeButton  = control.container.find( '.image-upload-remove-button' ),
1807
		    defaultButton = control.container.find( '.image-default-button' );
1808
1809
		control.container.find( '.kirki-controls-loading-spinner' ).hide();
1810
1811
		// Tweaks for save_as = id.
1812
		if ( ( 'id' === saveAs || 'ID' === saveAs ) && '' !== value ) {
1813
			wp.media.attachment( value ).fetch().then( function() {
1814
				setTimeout( function() {
1815
					var url = wp.media.attachment( value ).get( 'url' );
1816
					preview.removeClass().addClass( 'thumbnail thumbnail-image' ).html( '<img src="' + url + '" alt="" />' );
1817
				}, 700 );
1818
			} );
1819
		}
1820
1821
		// If value is not empty, hide the "default" button.
1822
		if ( ( 'url' === saveAs && '' !== value ) || ( 'array' === saveAs && ! _.isUndefined( value.url ) && '' !== value.url ) ) {
1823
			control.container.find( 'image-default-button' ).hide();
1824
		}
1825
1826
		// If value is empty, hide the "remove" button.
1827
		if ( ( 'url' === saveAs && '' === value ) || ( 'array' === saveAs && ( _.isUndefined( value.url ) || '' === value.url ) ) ) {
1828
			removeButton.hide();
1829
		}
1830
1831
		// If value is default, hide the default button.
1832
		if ( value === control.params['default'] ) {
1833
			control.container.find( 'image-default-button' ).hide();
1834
		}
1835
1836
		if ( '' !== previewImage ) {
1837
			preview.removeClass().addClass( 'thumbnail thumbnail-image' ).html( '<img src="' + previewImage + '" alt="" />' );
1838
		}
1839
1840
		control.container.on( 'click', '.image-upload-button', function( e ) {
1841
			var image = wp.media({ multiple: false }).open().on( 'select', function() {
1842
1843
					// This will return the selected image from the Media Uploader, the result is an object.
1844
					var uploadedImage = image.state().get( 'selection' ).first(),
1845
					    previewImage  = uploadedImage.toJSON().sizes.full.url;
1846
1847
					if ( ! _.isUndefined( uploadedImage.toJSON().sizes.medium ) ) {
1848
						previewImage = uploadedImage.toJSON().sizes.medium.url;
1849
					} else if ( ! _.isUndefined( uploadedImage.toJSON().sizes.thumbnail ) ) {
1850
						previewImage = uploadedImage.toJSON().sizes.thumbnail.url;
1851
					}
1852
1853
					if ( 'array' === saveAs ) {
1854
						control.saveValue( 'id', uploadedImage.toJSON().id );
1855
						control.saveValue( 'url', uploadedImage.toJSON().sizes.full.url );
1856
						control.saveValue( 'width', uploadedImage.toJSON().width );
1857
						control.saveValue( 'height', uploadedImage.toJSON().height );
1858
					} else if ( 'id' === saveAs ) {
1859
						control.saveValue( 'id', uploadedImage.toJSON().id );
1860
					} else {
1861
						control.saveValue( 'url', uploadedImage.toJSON().sizes.full.url );
1862
					}
1863
1864
					if ( preview.length ) {
1865
						preview.removeClass().addClass( 'thumbnail thumbnail-image' ).html( '<img src="' + previewImage + '" alt="" />' );
1866
					}
1867
					if ( removeButton.length ) {
1868
						removeButton.show();
1869
						defaultButton.hide();
1870
					}
1871
			    });
1872
1873
			e.preventDefault();
1874
		});
1875
1876
		control.container.on( 'click', '.image-upload-remove-button', function( e ) {
1877
1878
			var preview,
1879
			    removeButton,
1880
			    defaultButton;
1881
1882
			e.preventDefault();
1883
1884
			control.saveValue( 'id', '' );
1885
			control.saveValue( 'url', '' );
1886
			control.saveValue( 'width', '' );
1887
			control.saveValue( 'height', '' );
1888
1889
			preview       = control.container.find( '.placeholder, .thumbnail' );
1890
			removeButton  = control.container.find( '.image-upload-remove-button' );
1891
			defaultButton = control.container.find( '.image-default-button' );
1892
1893
			if ( preview.length ) {
1894
				preview.removeClass().addClass( 'placeholder' ).html( 'No file selected' );
1895
			}
1896
			if ( removeButton.length ) {
1897
				removeButton.hide();
1898
				if ( jQuery( defaultButton ).hasClass( 'button' ) ) {
1899
					defaultButton.show();
1900
				}
1901
			}
1902
		});
1903
1904
		control.container.on( 'click', '.image-default-button', function( e ) {
1905
1906
			var preview,
1907
			    removeButton,
1908
			    defaultButton;
1909
1910
			e.preventDefault();
1911
1912
			control.saveValue( 'url', control.params['default'] );
1913
1914
			preview       = control.container.find( '.placeholder, .thumbnail' );
1915
			removeButton  = control.container.find( '.image-upload-remove-button' );
1916
			defaultButton = control.container.find( '.image-default-button' );
1917
1918
			if ( preview.length ) {
1919
				preview.removeClass().addClass( 'thumbnail thumbnail-image' ).html( '<img src="' + control.params['default'] + '" alt="" />' );
1920
			}
1921
			if ( removeButton.length ) {
1922
				removeButton.show();
1923
				defaultButton.hide();
1924
			}
1925
		});
1926
	},
1927
1928
	/**
1929
	 * Gets the value.
1930
	 */
1931
	getValue: function() {
1932
		var control = this,
1933
		    value   = control.setting._value,
1934
		    saveAs  = ( ! _.isUndefined( control.params.choices ) && ! _.isUndefined( control.params.choices.save_as ) ) ? control.params.choices.save_as : 'url';
1935
1936
		if ( 'array' === saveAs && _.isString( value ) ) {
1937
			value = {
1938
				url: value
1939
			};
1940
		}
1941
		return value;
1942
	},
1943
1944
	/**
1945
	 * Saves the value.
1946
	 */
1947
	saveValue: function( property, value ) {
1948
		var control   = this,
1949
		    valueOld  = control.setting._value,
1950
		    saveAs    = ( ! _.isUndefined( control.params.choices ) && ! _.isUndefined( control.params.choices.save_as ) ) ? control.params.choices.save_as : 'url';
1951
1952
		if ( 'array' === saveAs ) {
1953
			if ( _.isString( valueOld ) ) {
1954
				valueOld = {};
1955
			}
1956
			valueOld[ property ] = value;
1957
			control.setting.set( valueOld );
1958
			control.container.find( 'button' ).trigger( 'change' );
1959
			return;
1960
		}
1961
		control.setting.set( value );
1962
		control.container.find( 'button' ).trigger( 'change' );
1963
	}
1964
});
1965
wp.customize.controlConstructor['kirki-multicheck'] = wp.customize.kirkiDynamicControl.extend({
1966
1967
	initKirkiControl: function() {
1968
1969
		var control = this;
1970
1971
		// Save the value
1972
		control.container.on( 'change', 'input', function() {
1973
			var value = [],
1974
			    i = 0;
1975
1976
			// Build the value as an object using the sub-values from individual checkboxes.
1977
			jQuery.each( control.params.choices, function( key ) {
1978
				if ( control.container.find( 'input[value="' + key + '"]' ).is( ':checked' ) ) {
1979
					value[ i ] = key;
1980
					i++;
1981
				}
1982
			});
1983
1984
			// Update the value in the customizer.
1985
			control.setting.set( value );
1986
		});
1987
	}
1988
});
1989
/* global kirkiControlLoader */
1990
wp.customize.controlConstructor['kirki-multicolor'] = wp.customize.Control.extend({
1991
1992
	// When we're finished loading continue processing
1993
	ready: function() {
1994
1995
		'use strict';
1996
1997
		var control = this;
1998
1999
		// Init the control.
2000
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
2001
			kirkiControlLoader( control );
2002
		} else {
2003
			control.initKirkiControl();
2004
		}
2005
	},
2006
2007
	initKirkiControl: function() {
2008
2009
		'use strict';
2010
2011
		var control = this,
2012
		    colors  = control.params.choices,
2013
		    keys    = Object.keys( colors ),
2014
		    value   = this.params.value,
2015
		    target  = control.container.find( '.iris-target' ),
2016
		    i       = 0,
2017
		    irisInput,
2018
		    irisPicker;
2019
2020
		// Proxy function that handles changing the individual colors
2021
		function kirkiMulticolorChangeHandler( control, value, subSetting ) {
2022
2023
			var picker = control.container.find( '.multicolor-index-' + subSetting ),
2024
			    args   = {
2025
					target: target[0],
2026
					change: function() {
2027
2028
						// Color controls require a small delay.
2029
						setTimeout( function() {
2030
2031
							// Set the value.
2032
							control.saveValue( subSetting, picker.val() );
2033
2034
							// Trigger the change.
2035
							control.container.find( '.multicolor-index-' + subSetting ).trigger( 'change' );
2036
						}, 100 );
2037
					}
2038
			    };
2039
2040
			if ( _.isObject( colors.irisArgs ) ) {
2041
				_.each( colors.irisArgs, function( irisValue, irisKey ) {
2042
					args[ irisKey ] = irisValue;
2043
				});
2044
			}
2045
2046
			// Did we change the value?
2047
			picker.wpColorPicker( args );
2048
		}
2049
2050
		// Colors loop
2051
		while ( i < Object.keys( colors ).length ) {
2052
2053
			kirkiMulticolorChangeHandler( this, value, keys[ i ] );
2054
2055
			// Move colorpicker to the 'iris-target' container div
2056
			irisInput  = control.container.find( '.wp-picker-container .wp-picker-input-wrap' ),
2057
			irisPicker = control.container.find( '.wp-picker-container .wp-picker-holder' );
2058
			jQuery( irisInput[0] ).detach().appendTo( target[0] );
2059
			jQuery( irisPicker[0] ).detach().appendTo( target[0] );
2060
2061
			i++;
2062
		}
2063
	},
2064
2065
	/**
2066
	 * Saves the value.
2067
	 */
2068
	saveValue: function( property, value ) {
2069
2070
		'use strict';
2071
2072
		var control   = this,
2073
		    input     = control.container.find( '.multicolor-hidden-value' ),
2074
		    valueJSON = jQuery( input ).val(),
2075
		    valueObj  = JSON.parse( valueJSON );
2076
2077
		valueObj[ property ] = value;
2078
		jQuery( input ).attr( 'value', JSON.stringify( valueObj ) ).trigger( 'change' );
2079
		control.setting.set( valueObj );
2080
	}
2081
});
2082
wp.customize.controlConstructor['kirki-number'] = wp.customize.kirkiDynamicControl.extend({
2083
2084
	initKirkiControl: function() {
2085
2086
		var control = this,
2087
		    value   = control.setting._value,
2088
		    html    = '',
2089
		    input,
2090
		    up,
2091
		    down;
2092
2093
		// Make sure we use default values if none are define for some arguments.
2094
		control.params.choices = _.defaults( control.params.choices, {
2095
			min: 0,
2096
			max: 100,
2097
			step: 1
2098
		} );
2099
2100
		// Make sure we have a valid value.
2101
		if ( isNaN( value ) || '' === value ) {
2102
			value = ( 0 > control.params.choices.min && 0 < control.params.choices.max ) ? 0 : control.params.choices.min;
2103
		}
2104
		value = parseFloat( value );
2105
2106
		// If step is 'any', set to 0.001.
2107
		control.params.choices.step = ( 'any' === control.params.choices.step ) ? 0.001 : control.params.choices.step;
2108
2109
		// Make sure choices are properly formtted as numbers.
2110
		control.params.choices.min  = parseFloat( control.params.choices.min );
2111
		control.params.choices.max  = parseFloat( control.params.choices.max );
2112
		control.params.choices.step = parseFloat( control.params.choices.step );
2113
2114
		// Build the HTML for the control.
2115
		html += '<label>';
2116
		if ( control.params.label ) {
2117
			html += '<span class="customize-control-title">' + control.params.label + '</span>';
2118
		}
2119
		if ( control.params.description ) {
2120
			html += '<span class="description customize-control-description">' + control.params.description + '</span>';
2121
		}
2122
		html += '<div class="customize-control-content">';
2123
		html += '<input ' + control.params.inputAttrs + ' type="text" ' + control.params.link + ' value="' + value + '" />';
2124
		html += '<div class="quantity button minus">-</div>';
2125
		html += '<div class="quantity button plus">+</div>';
2126
		html += '</div>';
2127
		html += '</label>';
2128
2129
		control.container.html( html );
2130
2131
		input = control.container.find( 'input' );
2132
		up    = control.container.find( '.plus' );
2133
		down  = control.container.find( '.minus' );
2134
2135
		up.click( function() {
2136
			var oldVal = parseFloat( input.val() ),
2137
			    newVal;
2138
2139
			newVal = ( oldVal >= control.params.choices.max ) ? oldVal : oldVal + control.params.choices.step;
2140
2141
			input.val( newVal );
2142
			input.trigger( 'change' );
2143
		} );
2144
2145
		down.click( function() {
2146
			var oldVal = parseFloat( input.val() ),
2147
			    newVal;
2148
2149
			newVal = ( oldVal <= control.params.choices.min ) ? oldVal : oldVal - control.params.choices.step;
2150
2151
			input.val( newVal );
2152
			input.trigger( 'change' );
2153
		} );
2154
2155
		this.container.on( 'change keyup paste click', 'input', function() {
2156
			control.setting.set( jQuery( this ).val() );
2157
		});
2158
	}
2159
});
2160
wp.customize.controlConstructor['kirki-palette'] = wp.customize.kirkiDynamicControl.extend({});
2161
/* global kirkiSetSettingValue */
2162
wp.customize.controlConstructor['kirki-preset'] = wp.customize.kirkiDynamicControl.extend({
2163
2164
	initKirkiControl: function() {
2165
2166
		var control = this,
2167
		    selectValue;
2168
2169
		// Trigger a change
2170
		this.container.on( 'change', 'select', function() {
2171
2172
			// Get the control's value
2173
			selectValue = jQuery( this ).val();
2174
2175
			// Update the value using the customizer API and trigger the "save" button
2176
			control.setting.set( selectValue );
2177
2178
			// We have to get the choices of this control
2179
			// and then start parsing them to see what we have to do for each of the choices.
2180
			jQuery.each( control.params.choices, function( key, value ) {
2181
2182
				// If the current value of the control is the key of the choice,
2183
				// then we can continue processing, Otherwise there's no reason to do anything.
2184
				if ( selectValue === key ) {
2185
2186
					// Each choice has an array of settings defined in it.
2187
					// We'll have to loop through them all and apply the changes needed to them.
2188
					jQuery.each( value.settings, function( presetSetting, presetSettingValue ) {
2189
						kirkiSetSettingValue.set( presetSetting, presetSettingValue );
2190
					});
2191
				}
2192
			});
2193
			wp.customize.previewer.refresh();
2194
		});
2195
	}
2196
});
2197
wp.customize.controlConstructor['kirki-radio-buttonset'] = wp.customize.kirkiDynamicControl.extend({});
2198
wp.customize.controlConstructor['kirki-radio-image'] = wp.customize.kirkiDynamicControl.extend({});
2199
/* global kirkiControlLoader */
2200
var RepeaterRow = function( rowIndex, container, label, control ) {
2201
2202
	'use strict';
2203
2204
	var self        = this;
2205
	this.rowIndex   = rowIndex;
2206
	this.container  = container;
2207
	this.label      = label;
2208
	this.header     = this.container.find( '.repeater-row-header' ),
2209
2210
	this.header.on( 'click', function() {
2211
		self.toggleMinimize();
2212
	});
2213
2214
	this.container.on( 'click', '.repeater-row-remove', function() {
2215
		self.remove();
2216
	});
2217
2218
	this.header.on( 'mousedown', function() {
2219
		self.container.trigger( 'row:start-dragging' );
2220
	});
2221
2222
	this.container.on( 'keyup change', 'input, select, textarea', function( e ) {
2223
		self.container.trigger( 'row:update', [ self.rowIndex, jQuery( e.target ).data( 'field' ), e.target ] );
2224
	});
2225
2226
	this.setRowIndex = function( rowIndex ) {
2227
		this.rowIndex = rowIndex;
2228
		this.container.attr( 'data-row', rowIndex );
2229
		this.container.data( 'row', rowIndex );
2230
		this.updateLabel();
2231
	};
2232
2233
	this.toggleMinimize = function() {
2234
2235
		// Store the previous state.
2236
		this.container.toggleClass( 'minimized' );
2237
		this.header.find( '.dashicons' ).toggleClass( 'dashicons-arrow-up' ).toggleClass( 'dashicons-arrow-down' );
2238
	};
2239
2240
	this.remove = function() {
2241
		this.container.slideUp( 300, function() {
2242
			jQuery( this ).detach();
2243
		});
2244
		this.container.trigger( 'row:remove', [ this.rowIndex ] );
2245
	};
2246
2247
	this.updateLabel = function() {
2248
		var rowLabelField,
2249
		    rowLabel,
2250
		    rowLabelSelector;
2251
2252
		if ( 'field' === this.label.type ) {
2253
			rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' );
2254
			if ( _.isFunction( rowLabelField.val ) ) {
2255
				rowLabel = rowLabelField.val();
2256
				if ( '' !== rowLabel ) {
2257
					if ( ! _.isUndefined( control.params.fields[ this.label.field ] ) ) {
2258
						if ( ! _.isUndefined( control.params.fields[ this.label.field ].type ) ) {
2259
							if ( 'select' === control.params.fields[ this.label.field ].type ) {
2260
								if ( ! _.isUndefined( control.params.fields[ this.label.field ].choices ) && ! _.isUndefined( control.params.fields[ this.label.field ].choices[ rowLabelField.val() ] ) ) {
2261
									rowLabel = control.params.fields[ this.label.field ].choices[ rowLabelField.val() ];
2262
								}
2263
							} else if ( 'radio' === control.params.fields[ this.label.field ].type || 'radio-image' === control.params.fields[ this.label.field ].type ) {
2264
								rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked';
2265
								rowLabel = jQuery( rowLabelSelector ).val();
2266
							}
2267
						}
2268
					}
2269
					this.header.find( '.repeater-row-label' ).text( rowLabel );
2270
					return;
2271
				}
2272
			}
2273
		}
2274
		this.header.find( '.repeater-row-label' ).text( this.label.value + ' ' + ( this.rowIndex + 1 ) );
2275
	};
2276
	this.updateLabel();
2277
};
2278
2279
wp.customize.controlConstructor.repeater = wp.customize.Control.extend({
2280
2281
	// When we're finished loading continue processing
2282
	ready: function() {
2283
2284
		'use strict';
2285
2286
		var control = this;
2287
2288
		// Init the control.
2289
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
2290
			kirkiControlLoader( control );
2291
		} else {
2292
			control.initKirkiControl();
2293
		}
2294
	},
2295
2296
	initKirkiControl: function() {
2297
2298
		'use strict';
2299
2300
		var control = this,
2301
		    limit,
2302
		    theNewRow;
2303
2304
		// The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
2305
		var settingValue = this.params.value;
2306
2307
		control.container.find( '.kirki-controls-loading-spinner' ).hide();
2308
2309
		// The hidden field that keeps the data saved (though we never update it)
2310
		this.settingField = this.container.find( '[data-customize-setting-link]' ).first();
2311
2312
		// Set the field value for the first time, we'll fill it up later
2313
		this.setValue( [], false );
2314
2315
		// The DIV that holds all the rows
2316
		this.repeaterFieldsContainer = this.container.find( '.repeater-fields' ).first();
2317
2318
		// Set number of rows to 0
2319
		this.currentIndex = 0;
2320
2321
		// Save the rows objects
2322
		this.rows = [];
2323
2324
		// Default limit choice
2325
		limit = false;
2326
		if ( ! _.isUndefined( this.params.choices.limit ) ) {
2327
			limit = ( 0 >= this.params.choices.limit ) ? false : parseInt( this.params.choices.limit, 10 );
2328
		}
2329
2330
		this.container.on( 'click', 'button.repeater-add', function( e ) {
2331
			e.preventDefault();
2332
			if ( ! limit || control.currentIndex < limit ) {
2333
				theNewRow = control.addRow();
2334
				theNewRow.toggleMinimize();
2335
				control.initColorPicker();
2336
				control.initSelect( theNewRow );
2337
			} else {
2338
				jQuery( control.selector + ' .limit' ).addClass( 'highlight' );
2339
			}
2340
		});
2341
2342
		this.container.on( 'click', '.repeater-row-remove', function() {
2343
			control.currentIndex--;
2344
			if ( ! limit || control.currentIndex < limit ) {
2345
				jQuery( control.selector + ' .limit' ).removeClass( 'highlight' );
2346
			}
2347
		});
2348
2349
		this.container.on( 'click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function( e ) {
2350
			e.preventDefault();
2351
			control.$thisButton = jQuery( this );
2352
			control.openFrame( e );
2353
		});
2354
2355
		this.container.on( 'click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function( e ) {
2356
			e.preventDefault();
2357
			control.$thisButton = jQuery( this );
2358
			control.removeImage( e );
2359
		});
2360
2361
		this.container.on( 'click keypress', '.repeater-field-upload .remove-button', function( e ) {
2362
			e.preventDefault();
2363
			control.$thisButton = jQuery( this );
2364
			control.removeFile( e );
2365
		});
2366
2367
		/**
2368
		 * Function that loads the Mustache template
2369
		 */
2370
		this.repeaterTemplate = _.memoize( function() {
2371
			var compiled,
2372
			    /*
2373
			     * Underscore's default ERB-style templates are incompatible with PHP
2374
			     * when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
2375
			     *
2376
			     * @see trac ticket #22344.
2377
			     */
2378
			    options = {
2379
					evaluate: /<#([\s\S]+?)#>/g,
2380
					interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
2381
					escape: /\{\{([^\}]+?)\}\}(?!\})/g,
2382
					variable: 'data'
2383
			    };
2384
2385
			return function( data ) {
2386
				compiled = _.template( control.container.find( '.customize-control-repeater-content' ).first().html(), null, options );
2387
				return compiled( data );
2388
			};
2389
		});
2390
2391
		// When we load the control, the fields have not been filled up
2392
		// This is the first time that we create all the rows
2393
		if ( settingValue.length ) {
2394
			_.each( settingValue, function( subValue ) {
2395
				theNewRow = control.addRow( subValue );
2396
				control.initColorPicker();
2397
				control.initSelect( theNewRow, subValue );
2398
			});
2399
		}
2400
2401
		// Once we have displayed the rows, we cleanup the values
2402
		this.setValue( settingValue, true, true );
2403
2404
		this.repeaterFieldsContainer.sortable({
2405
			handle: '.repeater-row-header',
2406
			update: function() {
2407
				control.sort();
2408
			}
2409
		});
2410
2411
	},
2412
2413
	/**
2414
	 * Open the media modal.
2415
	 */
2416
	openFrame: function( event ) {
2417
2418
		'use strict';
2419
2420
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
2421
			return;
2422
		}
2423
2424
		if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-cropped_image' ) ) {
2425
			this.initCropperFrame();
2426
		} else {
2427
			this.initFrame();
2428
		}
2429
2430
		this.frame.open();
2431
	},
2432
2433
	initFrame: function() {
2434
2435
		'use strict';
2436
2437
		var libMediaType = this.getMimeType();
2438
2439
		this.frame = wp.media({
2440
			states: [
2441
			new wp.media.controller.Library({
2442
					library:  wp.media.query({ type: libMediaType }),
2443
					multiple: false,
2444
					date:     false
2445
				})
2446
			]
2447
		});
2448
2449
		// When a file is selected, run a callback.
2450
		this.frame.on( 'select', this.onSelect, this );
2451
	},
2452
	/**
2453
	 * Create a media modal select frame, and store it so the instance can be reused when needed.
2454
	 * This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
2455
	 */
2456
	initCropperFrame: function() {
2457
2458
		'use strict';
2459
2460
		// We get the field id from which this was called
2461
		var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ),
2462
		    attrs          = [ 'width', 'height', 'flex_width', 'flex_height' ], // A list of attributes to look for
2463
		    libMediaType   = this.getMimeType();
2464
2465
		// Make sure we got it
2466
		if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
2467
2468
			// Make fields is defined and only do the hack for cropped_image
2469
			if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'cropped_image' === this.params.fields[ currentFieldId ].type ) {
2470
2471
				//Iterate over the list of attributes
2472
				attrs.forEach( function( el ) {
2473
2474
					// If the attribute exists in the field
2475
					if ( ! _.isUndefined( this.params.fields[ currentFieldId ][ el ] ) ) {
2476
2477
						// Set the attribute in the main object
2478
						this.params[ el ] = this.params.fields[ currentFieldId ][ el ];
2479
					}
2480
				}.bind( this ) );
2481
			}
2482
		}
2483
2484
		this.frame = wp.media({
2485
			button: {
2486
				text: 'Select and Crop',
2487
				close: false
2488
			},
2489
			states: [
2490
				new wp.media.controller.Library({
2491
					library:         wp.media.query({ type: libMediaType }),
2492
					multiple:        false,
2493
					date:            false,
2494
					suggestedWidth:  this.params.width,
2495
					suggestedHeight: this.params.height
2496
				}),
2497
				new wp.media.controller.CustomizeImageCropper({
2498
					imgSelectOptions: this.calculateImageSelectOptions,
2499
					control: this
2500
				})
2501
			]
2502
		});
2503
2504
		this.frame.on( 'select', this.onSelectForCrop, this );
2505
		this.frame.on( 'cropped', this.onCropped, this );
2506
		this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2507
2508
	},
2509
2510
	onSelect: function() {
2511
2512
		'use strict';
2513
2514
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2515
2516
		if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-upload' ) ) {
2517
			this.setFileInRepeaterField( attachment );
2518
		} else {
2519
			this.setImageInRepeaterField( attachment );
2520
		}
2521
	},
2522
2523
	/**
2524
	 * After an image is selected in the media modal, switch to the cropper
2525
	 * state if the image isn't the right size.
2526
	 */
2527
2528
	onSelectForCrop: function() {
2529
2530
		'use strict';
2531
2532
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2533
2534
		if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2535
			this.setImageInRepeaterField( attachment );
2536
		} else {
2537
			this.frame.setState( 'cropper' );
2538
		}
2539
	},
2540
2541
	/**
2542
	 * After the image has been cropped, apply the cropped image data to the setting.
2543
	 *
2544
	 * @param {object} croppedImage Cropped attachment data.
2545
	 */
2546
	onCropped: function( croppedImage ) {
2547
2548
		'use strict';
2549
2550
		this.setImageInRepeaterField( croppedImage );
2551
2552
	},
2553
2554
	/**
2555
	 * Returns a set of options, computed from the attached image data and
2556
	 * control-specific data, to be fed to the imgAreaSelect plugin in
2557
	 * wp.media.view.Cropper.
2558
	 *
2559
	 * @param {wp.media.model.Attachment} attachment
2560
	 * @param {wp.media.controller.Cropper} controller
2561
	 * @returns {Object} Options
2562
	 */
2563
	calculateImageSelectOptions: function( attachment, controller ) {
2564
2565
		'use strict';
2566
2567
		var control    = controller.get( 'control' ),
2568
		    flexWidth  = !! parseInt( control.params.flex_width, 10 ),
2569
		    flexHeight = !! parseInt( control.params.flex_height, 10 ),
2570
		    realWidth  = attachment.get( 'width' ),
2571
		    realHeight = attachment.get( 'height' ),
2572
		    xInit      = parseInt( control.params.width, 10 ),
2573
		    yInit      = parseInt( control.params.height, 10 ),
2574
		    ratio      = xInit / yInit,
2575
		    xImg       = realWidth,
2576
		    yImg       = realHeight,
2577
		    x1,
2578
		    y1,
2579
		    imgSelectOptions;
2580
2581
		controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2582
2583
		if ( xImg / yImg > ratio ) {
2584
			yInit = yImg;
2585
			xInit = yInit * ratio;
2586
		} else {
2587
			xInit = xImg;
2588
			yInit = xInit / ratio;
2589
		}
2590
2591
		x1 = ( xImg - xInit ) / 2;
2592
		y1 = ( yImg - yInit ) / 2;
2593
2594
		imgSelectOptions = {
2595
			handles:     true,
2596
			keys:        true,
2597
			instance:    true,
2598
			persistent:  true,
2599
			imageWidth:  realWidth,
2600
			imageHeight: realHeight,
2601
			x1:          x1,
2602
			y1:          y1,
2603
			x2:          xInit + x1,
2604
			y2:          yInit + y1
2605
		};
2606
2607
		if ( false === flexHeight && false === flexWidth ) {
2608
			imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2609
		}
2610
		if ( false === flexHeight ) {
2611
			imgSelectOptions.maxHeight = yInit;
2612
		}
2613
		if ( false === flexWidth ) {
2614
			imgSelectOptions.maxWidth = xInit;
2615
		}
2616
2617
		return imgSelectOptions;
2618
	},
2619
2620
	/**
2621
	 * Return whether the image must be cropped, based on required dimensions.
2622
	 *
2623
	 * @param {bool} flexW
2624
	 * @param {bool} flexH
2625
	 * @param {int}  dstW
2626
	 * @param {int}  dstH
2627
	 * @param {int}  imgW
2628
	 * @param {int}  imgH
2629
	 * @return {bool}
2630
	 */
2631
	mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2632
2633
		'use strict';
2634
2635
		if ( ( true === flexW && true === flexH ) || ( true === flexW && dstH === imgH ) || ( true === flexH && dstW === imgW ) || ( dstW === imgW && dstH === imgH ) || ( imgW <= dstW ) ) {
2636
			return false;
2637
		}
2638
2639
		return true;
2640
	},
2641
2642
	/**
2643
	 * If cropping was skipped, apply the image data directly to the setting.
2644
	 */
2645
	onSkippedCrop: function() {
2646
2647
		'use strict';
2648
2649
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2650
		this.setImageInRepeaterField( attachment );
2651
2652
	},
2653
2654
	/**
2655
	 * Updates the setting and re-renders the control UI.
2656
	 *
2657
	 * @param {object} attachment
2658
	 */
2659
	setImageInRepeaterField: function( attachment ) {
2660
2661
		'use strict';
2662
2663
		var $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image' );
2664
2665
		$targetDiv.find( '.kirki-image-attachment' ).html( '<img src="' + attachment.url + '">' ).hide().slideDown( 'slow' );
2666
2667
		$targetDiv.find( '.hidden-field' ).val( attachment.id );
2668
		this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
2669
		$targetDiv.find( '.remove-button' ).show();
2670
2671
		//This will activate the save button
2672
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
2673
		this.frame.close();
2674
2675
	},
2676
2677
	/**
2678
	 * Updates the setting and re-renders the control UI.
2679
	 *
2680
	 * @param {object} attachment
2681
	 */
2682
	setFileInRepeaterField: function( attachment ) {
2683
2684
		'use strict';
2685
2686
		var $targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
2687
2688
		$targetDiv.find( '.kirki-file-attachment' ).html( '<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>' ).hide().slideDown( 'slow' );
2689
2690
		$targetDiv.find( '.hidden-field' ).val( attachment.id );
2691
		this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
2692
		$targetDiv.find( '.upload-button' ).show();
2693
		$targetDiv.find( '.remove-button' ).show();
2694
2695
		//This will activate the save button
2696
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
2697
		this.frame.close();
2698
2699
	},
2700
2701
	getMimeType: function() {
2702
2703
		'use strict';
2704
2705
		// We get the field id from which this was called
2706
		var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' );
2707
2708
		// Make sure we got it
2709
		if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
2710
2711
			// Make fields is defined and only do the hack for cropped_image
2712
			if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'upload' === this.params.fields[ currentFieldId ].type ) {
2713
2714
				// If the attribute exists in the field
2715
				if ( ! _.isUndefined( this.params.fields[ currentFieldId ].mime_type ) ) {
2716
2717
					// Set the attribute in the main object
2718
					return this.params.fields[ currentFieldId ].mime_type;
2719
				}
2720
			}
2721
		}
2722
		return 'image';
2723
2724
	},
2725
2726
	removeImage: function( event ) {
2727
2728
		'use strict';
2729
2730
		var $targetDiv,
2731
		    $uploadButton;
2732
2733
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
2734
			return;
2735
		}
2736
2737
		$targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload' );
2738
		$uploadButton = $targetDiv.find( '.upload-button' );
2739
2740
		$targetDiv.find( '.kirki-image-attachment' ).slideUp( 'fast', function() {
2741
			jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
2742
		});
2743
		$targetDiv.find( '.hidden-field' ).val( '' );
2744
		$uploadButton.text( $uploadButton.data( 'label' ) );
2745
		this.$thisButton.hide();
2746
2747
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
2748
2749
	},
2750
2751
	removeFile: function( event ) {
2752
2753
		'use strict';
2754
2755
		var $targetDiv,
2756
		    $uploadButton;
2757
2758
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
2759
			return;
2760
		}
2761
2762
		$targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
2763
		$uploadButton = $targetDiv.find( '.upload-button' );
2764
2765
		$targetDiv.find( '.kirki-file-attachment' ).slideUp( 'fast', function() {
2766
			jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
2767
		});
2768
		$targetDiv.find( '.hidden-field' ).val( '' );
2769
		$uploadButton.text( $uploadButton.data( 'label' ) );
2770
		this.$thisButton.hide();
2771
2772
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
2773
2774
	},
2775
2776
	/**
2777
	 * Get the current value of the setting
2778
	 *
2779
	 * @return Object
2780
	 */
2781
	getValue: function() {
2782
2783
		'use strict';
2784
2785
		// The setting is saved in JSON
2786
		return JSON.parse( decodeURI( this.setting.get() ) );
2787
2788
	},
2789
2790
	/**
2791
	 * Set a new value for the setting
2792
	 *
2793
	 * @param newValue Object
2794
	 * @param refresh If we want to refresh the previewer or not
2795
	 */
2796
	setValue: function( newValue, refresh, filtering ) {
2797
2798
		'use strict';
2799
2800
		// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
2801
		var filteredValue = newValue,
2802
		    filter        = [];
2803
2804
		if ( filtering ) {
2805
			jQuery.each( this.params.fields, function( index, value ) {
2806
				if ( 'image' === value.type || 'cropped_image' === value.type || 'upload' === value.type ) {
2807
					filter.push( index );
2808
				}
2809
			});
2810
			jQuery.each( newValue, function( index, value ) {
2811
				jQuery.each( filter, function( ind, field ) {
2812
					if ( ! _.isUndefined( value[ field ] ) && ! _.isUndefined( value[ field ].id ) ) {
2813
						filteredValue[index][ field ] = value[ field ].id;
2814
					}
2815
				});
2816
			});
2817
		}
2818
2819
		this.setting.set( encodeURI( JSON.stringify( filteredValue ) ) );
2820
2821
		if ( refresh ) {
2822
2823
			// Trigger the change event on the hidden field so
2824
			// previewer refresh the website on Customizer
2825
			this.settingField.trigger( 'change' );
2826
		}
2827
	},
2828
2829
	/**
2830
	 * Add a new row to repeater settings based on the structure.
2831
	 *
2832
	 * @param data (Optional) Object of field => value pairs (undefined if you want to get the default values)
2833
	 */
2834
	addRow: function( data ) {
2835
2836
		'use strict';
2837
2838
		var control       = this,
2839
		    template      = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
2840
		    settingValue  = this.getValue(), // Get the current setting value.
2841
		    newRowSetting = {}, // Saves the new setting data.
2842
		    templateData, // Data to pass to the template
2843
		    newRow,
2844
		    i;
2845
2846
		if ( template ) {
2847
2848
			// The control structure is going to define the new fields
2849
			// We need to clone control.params.fields. Assigning it
2850
			// ould result in a reference assignment.
2851
			templateData = jQuery.extend( true, {}, control.params.fields );
2852
2853
			// But if we have passed data, we'll use the data values instead
2854
			if ( data ) {
2855
				for ( i in data ) {
2856
					if ( data.hasOwnProperty( i ) && templateData.hasOwnProperty( i ) ) {
2857
						templateData[ i ]['default'] = data[ i ];
2858
					}
2859
				}
2860
			}
2861
2862
			templateData.index = this.currentIndex;
2863
2864
			// Append the template content
2865
			template = template( templateData );
2866
2867
			// Create a new row object and append the element
2868
			newRow = new RepeaterRow(
2869
				control.currentIndex,
2870
				jQuery( template ).appendTo( control.repeaterFieldsContainer ),
2871
				control.params.row_label,
2872
				control
2873
			);
2874
2875
			newRow.container.on( 'row:remove', function( e, rowIndex ) {
2876
				control.deleteRow( rowIndex );
2877
			});
2878
2879
			newRow.container.on( 'row:update', function( e, rowIndex, fieldName, element ) {
2880
				control.updateField.call( control, e, rowIndex, fieldName, element );
2881
				newRow.updateLabel();
2882
			});
2883
2884
			// Add the row to rows collection
2885
			this.rows[ this.currentIndex ] = newRow;
2886
2887
			for ( i in templateData ) {
2888
				if ( templateData.hasOwnProperty( i ) ) {
2889
					newRowSetting[ i ] = templateData[ i ]['default'];
2890
				}
2891
			}
2892
2893
			settingValue[ this.currentIndex ] = newRowSetting;
2894
			this.setValue( settingValue, true );
2895
2896
			this.currentIndex++;
2897
2898
			return newRow;
2899
		}
2900
	},
2901
2902
	sort: function() {
2903
2904
		'use strict';
2905
2906
		var control     = this,
2907
		    $rows       = this.repeaterFieldsContainer.find( '.repeater-row' ),
2908
		    newOrder    = [],
2909
		    settings    = control.getValue(),
2910
		    newRows     = [],
2911
		    newSettings = [];
2912
2913
		$rows.each( function( i, element ) {
2914
			newOrder.push( jQuery( element ).data( 'row' ) );
2915
		});
2916
2917
		jQuery.each( newOrder, function( newPosition, oldPosition ) {
2918
			newRows[ newPosition ] = control.rows[ oldPosition ];
2919
			newRows[ newPosition ].setRowIndex( newPosition );
2920
2921
			newSettings[ newPosition ] = settings[ oldPosition ];
2922
		});
2923
2924
		control.rows = newRows;
2925
		control.setValue( newSettings );
2926
2927
	},
2928
2929
	/**
2930
	 * Delete a row in the repeater setting
2931
	 *
2932
	 * @param index Position of the row in the complete Setting Array
2933
	 */
2934
	deleteRow: function( index ) {
2935
2936
		'use strict';
2937
2938
		var currentSettings = this.getValue(),
2939
		    row,
2940
		    i,
2941
		    prop;
2942
2943
		if ( currentSettings[ index ] ) {
2944
2945
			// Find the row
2946
			row = this.rows[ index ];
2947
			if ( row ) {
2948
2949
				// Remove the row settings
2950
				delete currentSettings[ index ];
2951
2952
				// Remove the row from the rows collection
2953
				delete this.rows[ index ];
2954
2955
				// Update the new setting values
2956
				this.setValue( currentSettings, true );
2957
2958
			}
2959
2960
		}
2961
2962
		// Remap the row numbers
2963
		i = 1;
2964
		for ( prop in this.rows ) {
2965
			if ( this.rows.hasOwnProperty( prop ) && this.rows[ prop ] ) {
2966
				this.rows[ prop ].updateLabel();
2967
				i++;
2968
			}
2969
		}
2970
	},
2971
2972
	/**
2973
	 * Update a single field inside a row.
2974
	 * Triggered when a field has changed
2975
	 *
2976
	 * @param e Event Object
2977
	 */
2978
	updateField: function( e, rowIndex, fieldId, element ) {
2979
2980
		'use strict';
2981
2982
		var type,
2983
		    row,
2984
		    currentSettings;
2985
2986
		if ( ! this.rows[ rowIndex ] ) {
2987
			return;
2988
		}
2989
2990
		if ( ! this.params.fields[ fieldId ] ) {
2991
			return;
2992
		}
2993
2994
		type            = this.params.fields[ fieldId].type;
2995
		row             = this.rows[ rowIndex ];
2996
		currentSettings = this.getValue();
2997
2998
		element = jQuery( element );
2999
3000
		if ( _.isUndefined( currentSettings[ row.rowIndex ][ fieldId ] ) ) {
3001
			return;
3002
		}
3003
3004
		if ( 'checkbox' === type ) {
3005
			currentSettings[ row.rowIndex ][ fieldId ] = element.is( ':checked' );
3006
		} else {
3007
3008
			// Update the settings
3009
			currentSettings[ row.rowIndex ][ fieldId ] = element.val();
3010
		}
3011
		this.setValue( currentSettings, true );
3012
	},
3013
3014
	/**
3015
	 * Init the color picker on color fields
3016
	 * Called after AddRow
3017
	 *
3018
	 */
3019
	initColorPicker: function() {
3020
3021
		'use strict';
3022
3023
		var control     = this,
3024
		    colorPicker = control.container.find( '.color-picker-hex' ),
3025
		    options     = {},
3026
		    fieldId     = colorPicker.data( 'field' );
3027
3028
		// We check if the color palette parameter is defined.
3029
		if ( ! _.isUndefined( fieldId ) && ! _.isUndefined( control.params.fields[ fieldId ] ) && ! _.isUndefined( control.params.fields[ fieldId ].palettes ) && _.isObject( control.params.fields[ fieldId ].palettes ) ) {
3030
			options.palettes = control.params.fields[ fieldId ].palettes;
3031
		}
3032
3033
		// When the color picker value is changed we update the value of the field
3034
		options.change = function( event, ui ) {
3035
3036
			var currentPicker   = jQuery( event.target ),
3037
			    row             = currentPicker.closest( '.repeater-row' ),
3038
			    rowIndex        = row.data( 'row' ),
3039
			    currentSettings = control.getValue();
3040
3041
			currentSettings[ rowIndex ][ currentPicker.data( 'field' ) ] = ui.color.toString();
3042
			control.setValue( currentSettings, true );
3043
3044
		};
3045
3046
		// Init the color picker
3047
		if ( 0 !== colorPicker.length ) {
3048
			colorPicker.wpColorPicker( options );
3049
		}
3050
	},
3051
3052
	/**
3053
	 * Init the dropdown-pages field with selectWoo
3054
	 * Called after AddRow
3055
	 *
3056
	 * @param {object} theNewRow the row that was added to the repeater
3057
	 * @param {object} data the data for the row if we're initializing a pre-existing row
3058
	 *
3059
	 */
3060
	initSelect: function( theNewRow, data ) {
3061
3062
		'use strict';
3063
3064
		var control  = this,
3065
		    dropdown = theNewRow.container.find( '.repeater-field select' ),
3066
		    $select,
3067
		    dataField,
3068
		    multiple,
3069
		    selectWooOptions = {};
3070
3071
		if ( 0 === dropdown.length ) {
3072
			return;
3073
		}
3074
3075
		dataField = dropdown.data( 'field' );
3076
		multiple  = jQuery( dropdown ).data( 'multiple' );
3077
		if ( 'undefed' !== multiple && jQuery.isNumeric( multiple ) ) {
3078
			multiple = parseInt( multiple, 10 );
3079
			if ( 1 < multiple ) {
3080
				selectWooOptions.maximumSelectionLength = multiple;
3081
			}
3082
		}
3083
3084
		data = data || {};
3085
		data[ dataField ] = data[ dataField ] || '';
3086
3087
		$select = jQuery( dropdown ).selectWoo( selectWooOptions ).val( data[ dataField ] );
3088
3089
		this.container.on( 'change', '.repeater-field select', function( event ) {
3090
3091
			var currentDropdown = jQuery( event.target ),
3092
			    row             = currentDropdown.closest( '.repeater-row' ),
3093
			    rowIndex        = row.data( 'row' ),
3094
			    currentSettings = control.getValue();
3095
3096
			currentSettings[ rowIndex ][ currentDropdown.data( 'field' ) ] = jQuery( this ).val();
3097
			control.setValue( currentSettings );
3098
		});
3099
	}
3100
});
3101
wp.customize.controlConstructor['kirki-slider'] = wp.customize.kirkiDynamicControl.extend({
3102
3103
	initKirkiControl: function() {
3104
		var control      = this,
3105
		    changeAction = ( 'postMessage' === control.setting.transport ) ? 'mousemove change' : 'change',
3106
			rangeInput   = control.container.find( 'input[type="range"]' ),
3107
			textInput    = control.container.find( 'input[type="text"]' ),
3108
		    value        = control.setting._value;
3109
3110
		// Set the initial value in the text input.
3111
		textInput.attr( 'value', value );
3112
3113
		// If the range input value changes copy the value to the text input.
3114
		rangeInput.on( 'mousemove change', function() {
3115
			textInput.attr( 'value', rangeInput.val() );
3116
		} );
3117
3118
		// Save the value when the range input value changes.
3119
		// This is separate from the above because of the postMessage differences.
3120
		// If the control refreshes the preview pane,
3121
		// we don't want a refresh for every change
3122
		// but 1 final refresh when the value is changed.
3123
		rangeInput.on( changeAction, function() {
3124
			control.setting.set( rangeInput.val() );
3125
		} );
3126
3127
		// If the text input value changes,
3128
		// copy the value to the range input
3129
		// and then save.
3130
		textInput.on( 'input paste change', function() {
3131
			rangeInput.attr( 'value', textInput.val() );
3132
			control.setting.set( textInput.val() );
3133
		} );
3134
3135
		// If the reset button is clicked,
3136
		// set slider and text input values to default
3137
		// and hen save.
3138
		control.container.find( '.slider-reset' ).on( 'click', function() {
3139
			textInput.attr( 'value', control.params['default'] );
3140
			rangeInput.attr( 'value', control.params['default'] );
3141
			control.setting.set( textInput.val() );
3142
		} );
3143
	}
3144
});
3145
/* global kirkiControlLoader */
3146
wp.customize.controlConstructor['kirki-sortable'] = wp.customize.Control.extend({
3147
3148
	// When we're finished loading continue processing
3149
	ready: function() {
3150
3151
		'use strict';
3152
3153
		var control = this;
3154
3155
		// Init the control.
3156
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
3157
			kirkiControlLoader( control );
3158
		} else {
3159
			control.initKirkiControl();
3160
		}
3161
	},
3162
3163
	initKirkiControl: function() {
3164
3165
		'use strict';
3166
3167
		var control = this;
3168
3169
		control.container.find( '.kirki-controls-loading-spinner' ).hide();
3170
3171
		// Set the sortable container.
3172
		control.sortableContainer = control.container.find( 'ul.sortable' ).first();
3173
3174
		// Init sortable.
3175
		control.sortableContainer.sortable({
3176
3177
			// Update value when we stop sorting.
3178
			stop: function() {
3179
				control.updateValue();
3180
			}
3181
		}).disableSelection().find( 'li' ).each( function() {
3182
3183
			// Enable/disable options when we click on the eye of Thundera.
3184
			jQuery( this ).find( 'i.visibility' ).click( function() {
3185
				jQuery( this ).toggleClass( 'dashicons-visibility-faint' ).parents( 'li:eq(0)' ).toggleClass( 'invisible' );
3186
			});
3187
		}).click( function() {
3188
3189
			// Update value on click.
3190
			control.updateValue();
3191
		});
3192
	},
3193
3194
	/**
3195
	 * Updates the sorting list
3196
	 */
3197
	updateValue: function() {
3198
3199
		'use strict';
3200
3201
		var control = this,
3202
		    newValue = [];
3203
3204
		this.sortableContainer.find( 'li' ).each( function() {
3205
			if ( ! jQuery( this ).is( '.invisible' ) ) {
3206
				newValue.push( jQuery( this ).data( 'value' ) );
3207
			}
3208
		});
3209
		control.setting.set( newValue );
3210
	}
3211
});
3212
wp.customize.controlConstructor['kirki-switch'] = wp.customize.kirkiDynamicControl.extend({
3213
3214
	initKirkiControl: function() {
3215
3216
		'use strict';
3217
3218
		var control       = this,
3219
		    checkboxValue = control.setting._value;
3220
3221
		// Save the value
3222
		this.container.on( 'change', 'input', function() {
3223
			checkboxValue = ( jQuery( this ).is( ':checked' ) ) ? true : false;
3224
			control.setting.set( checkboxValue );
3225
		});
3226
	}
3227
});
3228
wp.customize.controlConstructor['kirki-toggle'] = wp.customize.kirkiDynamicControl.extend({
3229
3230
	initKirkiControl: function() {
3231
3232
		var control = this,
3233
		    checkboxValue = control.setting._value;
3234
3235
		// Save the value
3236
		this.container.on( 'change', 'input', function() {
3237
			checkboxValue = ( jQuery( this ).is( ':checked' ) ) ? true : false;
3238
			control.setting.set( checkboxValue );
3239
		});
3240
	}
3241
});
3242
/* global kirkiControlLoader, kirkiAllFonts */
3243
wp.customize.controlConstructor['kirki-typography'] = wp.customize.Control.extend({
3244
3245
	// When we're finished loading continue processing
3246
	ready: function() {
3247
3248
		'use strict';
3249
3250
		var control = this;
3251
3252
		// Init the control.
3253
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
3254
			kirkiControlLoader( control );
3255
		} else {
3256
			control.initKirkiControl();
3257
		}
3258
	},
3259
3260
	initKirkiControl: function() {
3261
3262
		'use strict';
3263
3264
		var control = this,
3265
		    value   = control.getValue(),
3266
		    picker;
3267
3268
		control.renderFontSelector();
3269
		control.renderBackupFontSelector();
3270
		control.renderVariantSelector();
3271
		control.renderSubsetSelector();
3272
3273
		// Font-size.
3274
		if ( control.params['default']['font-size'] ) {
3275
			this.container.on( 'change keyup paste', '.font-size input', function() {
3276
				control.saveValue( 'font-size', jQuery( this ).val() );
3277
			});
3278
		}
3279
3280
		// Line-height.
3281
		if ( control.params['default']['line-height'] ) {
3282
			this.container.on( 'change keyup paste', '.line-height input', function() {
3283
				control.saveValue( 'line-height', jQuery( this ).val() );
3284
			});
3285
		}
3286
3287
		// Margin-top.
3288
		if ( control.params['default']['margin-top'] ) {
3289
			this.container.on( 'change keyup paste', '.margin-top input', function() {
3290
				control.saveValue( 'margin-top', jQuery( this ).val() );
3291
			});
3292
		}
3293
3294
		// Margin-bottom.
3295
		if ( control.params['default']['margin-bottom'] ) {
3296
			this.container.on( 'change keyup paste', '.margin-bottom input', function() {
3297
				control.saveValue( 'margin-bottom', jQuery( this ).val() );
3298
			});
3299
		}
3300
3301
		// Letter-spacing.
3302
		if ( control.params['default']['letter-spacing'] ) {
3303
			value['letter-spacing'] = ( jQuery.isNumeric( value['letter-spacing'] ) ) ? value['letter-spacing'] + 'px' : value['letter-spacing'];
3304
			this.container.on( 'change keyup paste', '.letter-spacing input', function() {
3305
				value['letter-spacing'] = ( jQuery.isNumeric( jQuery( this ).val() ) ) ? jQuery( this ).val() + 'px' : jQuery( this ).val();
3306
				control.saveValue( 'letter-spacing', value['letter-spacing'] );
3307
			});
3308
		}
3309
3310
		// Word-spacing.
3311
		if ( control.params['default']['word-spacing'] ) {
3312
			this.container.on( 'change keyup paste', '.word-spacing input', function() {
3313
				control.saveValue( 'word-spacing', jQuery( this ).val() );
3314
			});
3315
		}
3316
3317
		// Text-align.
3318
		if ( control.params['default']['text-align'] ) {
3319
			this.container.on( 'change', '.text-align input', function() {
3320
				control.saveValue( 'text-align', jQuery( this ).val() );
3321
			});
3322
		}
3323
3324
		// Text-transform.
3325
		if ( control.params['default']['text-transform'] ) {
3326
			jQuery( control.selector + ' .text-transform select' ).selectWoo().on( 'change', function() {
3327
				control.saveValue( 'text-transform', jQuery( this ).val() );
3328
			});
3329
		}
3330
3331
		// Color.
3332
		if ( control.params['default'].color ) {
3333
			picker = this.container.find( '.kirki-color-control' );
3334
			picker.wpColorPicker({
3335
				change: function() {
3336
					setTimeout( function() {
3337
						control.saveValue( 'color', picker.val() );
3338
					}, 100 );
3339
				}
3340
			});
3341
		}
3342
	},
3343
3344
	/**
3345
	 * Adds the font-families to the font-family dropdown
3346
	 * and instantiates selectWoo.
3347
	 */
3348
	renderFontSelector: function() {
3349
3350
		var control         = this,
3351
		    selector        = control.selector + ' .font-family select',
3352
		    data            = [],
3353
		    standardFonts   = [],
3354
		    googleFonts     = [],
3355
		    value           = control.getValue(),
3356
		    fonts           = control.getFonts(),
3357
		    fontSelect;
3358
3359
		// Format standard fonts as an array.
3360
		if ( ! _.isUndefined( fonts.standard ) ) {
3361
			_.each( fonts.standard, function( font ) {
3362
				standardFonts.push({
3363
					id: font.family.replace( /&quot;/g, '&#39' ),
3364
					text: font.label
3365
				});
3366
			});
3367
		}
3368
3369
		// Format google fonts as an array.
3370
		if ( ! _.isUndefined( fonts.standard ) ) {
3371
			_.each( fonts.google, function( font ) {
3372
				googleFonts.push({
3373
					id: font.family,
3374
					text: font.label
3375
				});
3376
			});
3377
		}
3378
3379
		// Combine forces and build the final data.
3380
		data = [
3381
			{ text: 'Standard Fonts', children: standardFonts },
3382
			{ text: 'Google Fonts',   children: googleFonts }
3383
		];
3384
3385
		// Instantiate selectWoo with the data.
3386
		fontSelect = jQuery( selector ).selectWoo({
3387
			data: data
3388
		});
3389
3390
		// Set the initial value.
3391
		if ( value['font-family'] ) {
3392
			fontSelect.val( value['font-family'].replace( /'/g, '"' ) ).trigger( 'change' );
3393
		}
3394
3395
		// When the value changes
3396
		fontSelect.on( 'change', function() {
3397
3398
			// Set the value.
3399
			control.saveValue( 'font-family', jQuery( this ).val() );
3400
3401
			// Re-init the font-backup selector.
3402
			control.renderBackupFontSelector();
3403
3404
			// Re-init variants selector.
3405
			control.renderVariantSelector();
3406
3407
			// Re-init subsets selector.
3408
			control.renderSubsetSelector();
3409
		});
3410
	},
3411
3412
	/**
3413
	 * Adds the font-families to the font-family dropdown
3414
	 * and instantiates selectWoo.
3415
	 */
3416
	renderBackupFontSelector: function() {
3417
3418
		var control       = this,
3419
		    selector      = control.selector + ' .font-backup select',
3420
		    standardFonts = [],
3421
		    value         = control.getValue(),
3422
		    fontFamily    = value['font-family'],
3423
		    variants      = control.getVariants( fontFamily ),
3424
		    fonts         = control.getFonts(),
3425
		    fontSelect;
3426
3427
		if ( _.isUndefined( value['font-backup'] ) || null === value['font-backup'] ) {
3428
			value['font-backup'] = '';
3429
		}
3430
3431
		// Hide if we're not on a google-font.
3432
		if ( false !== variants ) {
3433
			jQuery( control.selector + ' .font-backup' ).show();
3434
		} else {
3435
			jQuery( control.selector + ' .font-backup' ).hide();
3436
		}
3437
3438
		// Format standard fonts as an array.
3439
		if ( ! _.isUndefined( fonts.standard ) ) {
3440
			_.each( fonts.standard, function( font ) {
3441
				standardFonts.push({
3442
					id: font.family.replace( /&quot;/g, '&#39' ),
3443
					text: font.label
3444
				});
3445
			});
3446
		}
3447
3448
		// Instantiate selectWoo with the data.
3449
		fontSelect = jQuery( selector ).selectWoo({
3450
			data: standardFonts
3451
		});
3452
3453
		// Set the initial value.
3454
		if ( 'undefined' !== typeof value['font-backup'] ) {
3455
			fontSelect.val( value['font-backup'].replace( /'/g, '"' ) ).trigger( 'change' );
3456
		}
3457
3458
		// When the value changes
3459
		fontSelect.on( 'change', function() {
3460
3461
			// Set the value.
3462
			control.saveValue( 'font-backup', jQuery( this ).val() );
3463
		});
3464
	},
3465
3466
	/**
3467
	 * Renders the variants selector using selectWoo
3468
	 * Displays font-variants for the currently selected font-family.
3469
	 */
3470
	renderVariantSelector: function() {
3471
3472
		var control    = this,
3473
		    value      = control.getValue(),
3474
		    fontFamily = value['font-family'],
3475
		    variants   = control.getVariants( fontFamily ),
3476
		    selector   = control.selector + ' .variant select',
3477
		    data       = [],
3478
		    isValid    = false,
3479
		    fontWeight,
3480
		    variantSelector,
3481
		    fontStyle;
3482
3483
		if ( false !== variants ) {
3484
			jQuery( control.selector + ' .variant' ).show();
3485
			_.each( variants, function( variant ) {
3486
				if ( value.variant === variant.id ) {
3487
					isValid = true;
3488
				}
3489
				data.push({
3490
					id: variant.id,
3491
					text: variant.label
3492
				});
3493
			});
3494
			if ( ! isValid ) {
3495
				value.variant = 'regular';
3496
			}
3497
3498
			if ( jQuery( selector ).hasClass( 'select2-hidden-accessible' ) ) {
3499
				jQuery( selector ).selectWoo( 'destroy' );
3500
				jQuery( selector ).empty();
3501
			}
3502
3503
			// Instantiate selectWoo with the data.
3504
			variantSelector = jQuery( selector ).selectWoo({
3505
				data: data
3506
			});
3507
			variantSelector.val( value.variant ).trigger( 'change' );
3508
			variantSelector.on( 'change', function() {
3509
				control.saveValue( 'variant', jQuery( this ).val() );
3510
3511
				fontWeight = ( ! _.isString( value.variant ) ) ? '400' : value.variant.match( /\d/g );
3512
				fontWeight = ( ! _.isObject( fontWeight ) ) ? '400' : fontWeight.join( '' );
3513
				fontStyle  = ( -1 !== value.variant.indexOf( 'italic' ) ) ? 'italic' : 'normal';
3514
3515
				control.saveValue( 'font-weight', fontWeight );
3516
				control.saveValue( 'font-style', fontStyle );
3517
			});
3518
		} else {
3519
			jQuery( control.selector + ' .variant' ).hide();
3520
		}
3521
	},
3522
3523
	/**
3524
	 * Renders the subsets selector using selectWoo
3525
	 * Displays font-subsets for the currently selected font-family.
3526
	 */
3527
	renderSubsetSelector: function() {
3528
3529
		var control    = this,
3530
		    value      = control.getValue(),
3531
		    fontFamily = value['font-family'],
3532
		    subsets    = control.getSubsets( fontFamily ),
3533
		    selector   = control.selector + ' .subsets select',
3534
		    data       = [],
3535
		    validValue = value.subsets,
3536
		    subsetSelector;
3537
3538
		if ( false !== subsets ) {
3539
			jQuery( control.selector + ' .subsets' ).show();
3540
			_.each( subsets, function( subset ) {
3541
3542
				if ( _.isObject( validValue ) ) {
3543
					if ( -1 === validValue.indexOf( subset.id ) ) {
3544
						validValue = _.reject( validValue, function( subValue ) {
3545
							return subValue === subset.id;
3546
						});
3547
					}
3548
				}
3549
3550
				data.push({
3551
					id: subset.id,
3552
					text: subset.label
3553
				});
3554
			});
3555
3556
		} else {
3557
			jQuery( control.selector + ' .subsets' ).hide();
3558
		}
3559
3560
		if ( jQuery( selector ).hasClass( 'select2-hidden-accessible' ) ) {
3561
			jQuery( selector ).selectWoo( 'destroy' );
3562
			jQuery( selector ).empty();
3563
		}
3564
3565
		// Instantiate selectWoo with the data.
3566
		subsetSelector = jQuery( selector ).selectWoo({
3567
			data: data
3568
		});
3569
		subsetSelector.val( validValue ).trigger( 'change' );
3570
		subsetSelector.on( 'change', function() {
3571
			control.saveValue( 'subsets', jQuery( this ).val() );
3572
		});
3573
	},
3574
3575
	/**
3576
	 * Get fonts.
3577
	 */
3578
	getFonts: function() {
3579
		var control = this;
3580
3581
		if ( ! _.isUndefined( window[ 'kirkiFonts' + control.id ] ) ) {
3582
			return window[ 'kirkiFonts' + control.id ];
3583
		}
3584
		if ( 'undefined' !== typeof kirkiAllFonts ) {
3585
			return kirkiAllFonts;
3586
		}
3587
		return {
3588
			google: [],
3589
			standard: []
3590
		};
3591
	},
3592
3593
	/**
3594
	 * Get variants for a font-family.
3595
	 */
3596
	getVariants: function( fontFamily ) {
3597
		var control = this,
3598
		    fonts   = control.getFonts();
3599
3600
		var variants = false;
3601
		_.each( fonts.standard, function( font ) {
3602
			if ( fontFamily && font.family === fontFamily.replace( /'/g, '"' ) ) {
3603
				variants = font.variants;
3604
				return font.variants;
3605
			}
3606
		});
3607
3608
		_.each( fonts.google, function( font ) {
3609
			if ( font.family === fontFamily ) {
3610
				variants = font.variants;
3611
				return font.variants;
3612
			}
3613
		});
3614
		return variants;
3615
	},
3616
3617
	/**
3618
	 * Get subsets for a font-family.
3619
	 */
3620
	getSubsets: function( fontFamily ) {
3621
3622
		var control = this,
3623
		    subsets = false,
3624
		    fonts   = control.getFonts();
3625
3626
		_.each( fonts.google, function( font ) {
3627
			if ( font.family === fontFamily ) {
3628
				subsets = font.subsets;
3629
			}
3630
		});
3631
		return subsets;
3632
	},
3633
3634
	/**
3635
	 * Gets the value.
3636
	 */
3637
	getValue: function() {
3638
3639
		'use strict';
3640
3641
		var control   = this,
3642
		    input     = control.container.find( '.typography-hidden-value' ),
3643
		    valueJSON = jQuery( input ).val();
3644
3645
		return JSON.parse( valueJSON );
3646
	},
3647
3648
	/**
3649
	 * Saves the value.
3650
	 */
3651
	saveValue: function( property, value ) {
3652
3653
		'use strict';
3654
3655
		var control   = this,
3656
		    input     = control.container.find( '.typography-hidden-value' ),
3657
		    valueJSON = jQuery( input ).val(),
3658
		    valueObj  = JSON.parse( valueJSON );
3659
3660
		valueObj[ property ] = value;
3661
		wp.customize.control( control.id ).setting.set( valueObj );
3662
		jQuery( input ).attr( 'value', JSON.stringify( valueObj ) ).trigger( 'change' );
3663
	}
3664
});
3665